<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Certificates on ln --help</title>
    <link>https://blog.mei-home.net/tags/certificates/</link>
    <description>Recent content in Certificates on ln --help</description>
    <generator>Hugo -- 0.152.2</generator>
    <language>en</language>
    <lastBuildDate>Mon, 07 Apr 2025 20:41:41 +0200</lastBuildDate>
    <atom:link href="https://blog.mei-home.net/tags/certificates/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Nomad to k8s, Part 24: Migrating Vault to Kubernetes</title>
      <link>https://blog.mei-home.net/posts/k8s-migration-24-vault/</link>
      <pubDate>Mon, 07 Apr 2025 20:41:41 +0200</pubDate>
      <guid>https://blog.mei-home.net/posts/k8s-migration-24-vault/</guid>
      <description>Migrating my baremetal Vault to the Kubernetes cluster.</description>
      <content:encoded><![CDATA[<p>Wherein I migrate my HashiCorp Vault instance to the Kubernetes cluster.</p>
<p>This is part 25 of my <a href="https://blog.mei-home.net/tags/k8s-migration/">k8s migration series</a>.</p>
<p>Look at all this Yak wool. That&rsquo;s how much it takes to migrate <a href="https://www.vaultproject.io/">Vault</a> from
baremetal to a Kubernetes deployment. I&rsquo;ve been going back and forth for quite
a while, trying to decide what to do with my Vault instance. It&rsquo;s the one piece
of HashiCorp software I do not currently plan to get rid of. But there was a
problem: My Vault, or rather the High Availability nature of it, relied on
HashiCorp&rsquo;s <a href="https://www.consul.io/">Consul</a> and its DNS service discovery
functionality. And while I did want to keep Vault, I did not want to keep
Consul. And I also didn&rsquo;t really want to introduce some other sort of method,
like <a href="https://www.haproxy.org/">HAProxy</a>.</p>
<p>In the end, I sat down and thought quite hard for quite a while, mostly
thinking about potential reasons for why I should not move Vault to the Kubernetes
cluster. My main worry is bootstrapping - what happens if my entire Homelab goes
down, unplanned, and all at once? Be it because I stumble over the absolutely
wrong cable, or because my oven develops a short again and throws the main fuse.
Could I still get my Homelab back up and do any massaging it might need?</p>
<p>I ended up deciding that Vault on Kubernetes should be fine. All Kubernetes
Secrets are synced into the cluster anyway, and any other secrets I might need
also live in my password manager. It should be fine. Watch this space for the
day I find out what I overlooked. &#x1f605;</p>
<p>And thus began the Yak shaving.</p>
<h2 id="vault">Vault</h2>
<p>But before we start onto that mountain of wool, let&rsquo;s take a short detour and
look at what Vault is and what I use it for. Brought down to the simplest terms,
HashiCorp&rsquo;s Vault is an API server for secrets of many, many different kinds.
It supports everything from simple key-value secrets to PKI certificates.
It can also serve short-lived tokens, including for HashiCorp&rsquo;s other products
like Consul or Nomad. I used it for a number of things over the years.</p>
<p>The most important part of it is the <a href="https://developer.hashicorp.com/vault/docs/secrets/kv">KV store</a>
for me. It stores all manner of passwords, keys and certificates, like my public
cert. And it makes all of those available, given proper authorization, over HTTP.
I use secrets from this store for my Ansible playbooks, the Mastodon secrets via
<a href="https://external-secrets.io/latest/">external-secrets</a> in my Kubernetes cluster
and in my image generation setup for new hosts as well.
Support for it is very widespread as well. In HashiCorp&rsquo;s own tools of course,
but also in other tools like Ansible, where you shouldn&rsquo;t confuse it with
Ansible&rsquo;s own Vault secret store.</p>
<p>In the past, I also used the <a href="https://developer.hashicorp.com/vault/docs/secrets/nomad">Nomad secrets engine</a>
to get a short-lived token for Nomad API access for my backup solution.</p>
<p>Another big use case for me is as an internal, self-signed CA. During my Nomad/Vault/Consul
cluster days, this was pretty important functionality, because those self-signed
certs were used by all three components of my Homelab to secure their HTTPS
communication. I&rsquo;ve even gone to the length of installing the CA on all of my
devices, so I don&rsquo;t get any untrusted certificate warnings when accessing
services secured with that CA.
Since the introduction of Kubernetes, I&rsquo;m not using the Homelab CA quite as
much, but there are still a few internal things secured with it.</p>
<p>For a short while, I even considered using Vault as my OIDC identity provider,
but in the end I decided against it. My main reason for that was that I would
have needed to hang my internal secret store into the public internet, because
I intended to use OIDC for some public sites. And even though I&rsquo;ve got no reason
to distrust HashiCorp&rsquo;s security practices, and I could have only made certain
paths publicly accessible, I decided against it.</p>
<p>So what does working with Vault actually look like? The main interface is the
Vault CLI executable. You can control anything you need from the command line.
But it also provides a WebUI, if that&rsquo;s more your cup of tea. I never bothered
with it.</p>
<p>The first step of working with Vault is to obtain a token for all further tasks.
For this, Vault offers <a href="https://developer.hashicorp.com/vault/docs/auth">a plethora</a>
of auth methods, ranging from the good old username/password to OIDC or TLS certs.
I&rsquo;m using the <a href="https://developer.hashicorp.com/vault/docs/auth/userpass">userpass</a>
method, which is just good old username+password. It&rsquo;s comfortable for me, I can
use my password manager and just copy+paste the password in. It looks something
like this:</p>
<pre tabindex="0"><code class="language-shel" data-lang="shel">vault login -method=userpass username=myuser
Password (will be hidden):
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run &#34;vault login&#34;
again. Future Vault requests will automatically use this token.

Key                    Value
---                    -----
token                  hvs.CAESII0RlV4BS_5_A2q8mIpzYxiye0XoE-_Vvlb0YIAYfl-6Gh4KHGh2cy5sSmpvZk5QMXN2QW0wZ0c0R1A3cXV3TkQ
token_accessor         5ofJhWq55yZGOk6CJVRyBacd
token_duration         4h
token_renewable        true
token_policies         [&#34;admin&#34; &#34;default&#34;]
identity_policies      []
policies               [&#34;admin&#34; &#34;default&#34;]
token_meta_username    myuser
</code></pre><p>Don&rsquo;t worry, this token has long since expired. &#x1f642;
When you use <code>vault login</code>, Vault automatically puts the received token into a
file in <code>~/.vault-token</code>. And the <code>vault</code> CLI as well as other things with Vault
integration check that path as well.</p>
<p>As you&rsquo;d expect from a properly secured application, the tokens you&rsquo;re getting
have a restricted TTL. How long a token is initially valid can be configured,
in addition to enabling token renewal and defining an upper bound on how long
a token can live under any circumstances.</p>
<p>Then there&rsquo;s also the policies. Those define what the holder of a token can
actually do with it. In this case, I&rsquo;m having the <code>default</code> and <code>admin</code> policies.
The <code>default</code> policy mostly allows the holder to access information about the
token they&rsquo;re using, while <code>admin</code> is my admin policy, allowing full access to
Vault. It looks something like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">path</span> <span style="color:#e6db74">&#34;sys/health&#34;</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  capabilities <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;read&#34;, &#34;sudo&#34;</span>]
</span></span><span style="display:flex;"><span>}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># Create and manage ACL policies broadly across Vault
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># List existing policies
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">path</span> <span style="color:#e6db74">&#34;sys/policies/acl&#34;</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  capabilities <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;list&#34;</span>]
</span></span><span style="display:flex;"><span>}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># Create and manage ACL policies
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">path</span> <span style="color:#e6db74">&#34;sys/policies/acl/*&#34;</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  capabilities <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;create&#34;, &#34;read&#34;, &#34;update&#34;, &#34;delete&#34;, &#34;list&#34;, &#34;sudo&#34;</span>]
</span></span><span style="display:flex;"><span>}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># Enable and manage authentication methods broadly across Vault
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># Manage auth methods broadly across Vault
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">path</span> <span style="color:#e6db74">&#34;auth/*&#34;</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  capabilities <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;create&#34;, &#34;read&#34;, &#34;update&#34;, &#34;delete&#34;, &#34;list&#34;, &#34;sudo&#34;</span>]
</span></span><span style="display:flex;"><span>}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># Create, update, and delete auth methods
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">path</span> <span style="color:#e6db74">&#34;sys/auth/*&#34;</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  capabilities <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;create&#34;, &#34;update&#34;, &#34;delete&#34;, &#34;sudo&#34;</span>]
</span></span><span style="display:flex;"><span>}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># List auth methods
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">path</span> <span style="color:#e6db74">&#34;sys/auth&#34;</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  capabilities <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;read&#34;</span>]
</span></span><span style="display:flex;"><span>}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># Enable and manage the key/value secrets engine at `secret/` path
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># List, create, update, and delete key/value secrets
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">path</span> <span style="color:#e6db74">&#34;secret/*&#34;</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  capabilities <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;create&#34;, &#34;read&#34;, &#34;update&#34;, &#34;delete&#34;, &#34;list&#34;, &#34;sudo&#34;</span>]
</span></span><span style="display:flex;"><span>}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># Manage secrets engines
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">path</span> <span style="color:#e6db74">&#34;sys/mounts/*&#34;</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  capabilities <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;create&#34;, &#34;read&#34;, &#34;update&#34;, &#34;delete&#34;, &#34;list&#34;, &#34;sudo&#34;</span>]
</span></span><span style="display:flex;"><span>}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># Manage secrets engines
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">path</span> <span style="color:#e6db74">&#34;sys/remount&#34;</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  capabilities <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;create&#34;, &#34;read&#34;, &#34;update&#34;, &#34;delete&#34;, &#34;list&#34;, &#34;sudo&#34;</span>]
</span></span><span style="display:flex;"><span>}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># List existing secrets engines.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">path</span> <span style="color:#e6db74">&#34;sys/mounts&#34;</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  capabilities <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;read&#34;</span>]
</span></span><span style="display:flex;"><span>}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># Homenet Root CA access
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">path</span> <span style="color:#e6db74">&#34;homenet-ca*&#34;</span> {
</span></span><span style="display:flex;"><span>  capabilities <span style="color:#f92672">=</span> [ <span style="color:#e6db74">&#34;create&#34;, &#34;read&#34;, &#34;update&#34;, &#34;delete&#34;, &#34;list&#34;, &#34;sudo&#34;</span> ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Armed with this token, I can then for example take a look at my secrets:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>vault read secret/s3_users/blog
</span></span><span style="display:flex;"><span>Key                 Value
</span></span><span style="display:flex;"><span>---                 -----
</span></span><span style="display:flex;"><span>refresh_interval    768h
</span></span><span style="display:flex;"><span>access              abcde
</span></span><span style="display:flex;"><span>custom_metadata     map<span style="color:#f92672">[</span>managed-by:external-secrets<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>secret              <span style="color:#ae81ff">12345</span>
</span></span></code></pre></div><p>This is a pretty nice example, in fact. It shows that the <code>blog</code> secret consists
of two entries, <code>access</code> and <code>secret</code>, containing the standard S3 credentials.
But it also has <code>custom_metadata</code> indicating that it wasn&rsquo;t actually created
by me by hand, but was pushed into Vault via an external-secrets <a href="https://external-secrets.io/latest/api/pushsecret/">PushSecret</a>.
I&rsquo;m doing this because I need the S3 credentials for my blog in both, an Ansible
playbook I use to configure S3 buckets, and in the K8s cluster, because that&rsquo;s
where the bucket and credentials are created by Rook Ceph.</p>
<p>To put that same secret into Vault, the following command line could be used:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>vault kv put secret/s3_users/blog access<span style="color:#f92672">=</span>abcde secret<span style="color:#f92672">=</span><span style="color:#ae81ff">12345</span>
</span></span></code></pre></div><p>This would of course have the downside of putting the secret into the shell
history, unless a space is added at the front.
If you&rsquo;d prefer having Vault take the secret from stdin, you can run the same
command like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>vault kv put secret/s3_users/blog access<span style="color:#f92672">=</span>abcde secret<span style="color:#f92672">=</span>-
</span></span></code></pre></div><p>This will take the <code>access</code> key from the parameter, but for the <code>secret</code>, it
will ask you for the value, which keeps it out of the shell history.
But this approach also has a downside, because only one key can be used with
the <code>-</code> as input.
If you have more actually secret parameters, you can also put all of them into
a JSON file. I will demonstrate that later on when I migrate my Vault content
from my baremetal instance to the Kubernetes deployment.</p>
<p>If you want to use Vault values from within Ansible, I&rsquo;ve found the <a href="https://docs.ansible.com/ansible/latest/collections/community/hashi_vault/hashi_vault_lookup.html">Vault lookup</a>
pretty nice to use. It can be used  like this, to set a variable in a playbook:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">hosts</span>: <span style="color:#ae81ff">all</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Demonstration</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tags</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">demo</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">vars</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">s3_access</span>: <span style="color:#e6db74">&#34;{{ lookup(&#39;hashi_vault&#39;, &#39;secret=secret/s3_users/blog:access token=&#39;+vault_token+&#39; url=&#39;+vault_url) }}&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">s3_secret</span>: <span style="color:#e6db74">&#34;{{ lookup(&#39;hashi_vault&#39;, &#39;secret=secret/s3_users/blog:secret token=&#39;+vault_token+&#39; url=&#39;+vault_url) }}&#34;</span>
</span></span></code></pre></div><p>I&rsquo;m setting the <code>vault_token</code> with Ansible&rsquo;s <a href="https://docs.ansible.com/ansible/latest/collections/ansible/builtin/file_lookup.html">file lookup</a> like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">vault_token</span>: <span style="color:#e6db74">&#34;{{ lookup(&#39;file&#39;, &#39;/home/my_user/.vault-token&#39;) }}&#34;</span>
</span></span></code></pre></div><p>And because that file is automatically updated when the <code>vault login</code> command is
used, I&rsquo;m getting the current token automatically.</p>
<p>I will go into a bit more detail about generating certificates later as part
of the Vault k8s setup.</p>
<h2 id="setting-up-the-helm-chart">Setting up the Helm chart</h2>
<p>Alright. Let the Yak shaving finally commence. First of all, it&rsquo;s notable that there is
no official way to migrate the content of an instance to another instance. So I
had to go with setting up a completely new instance of Vault on k8s, instead of
doing some sort of migration.</p>
<p>So the first step was to configure and deploy the <a href="https://github.com/hashicorp/vault-helm">official Helm chart</a>,
following <a href="https://developer.hashicorp.com/vault/tutorials/kubernetes/kubernetes-raft-deployment-guide">this guide</a>.</p>
<p>And here is the result:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">global</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tlsDisable</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">openshift</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">serverTelemetry</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">prometheusOperator</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">injector</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">server</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">logLevel</span>: <span style="color:#ae81ff">debug</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">logFormat</span>: <span style="color:#ae81ff">json</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">memory</span>: <span style="color:#ae81ff">500Mi</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">cpu</span>: <span style="color:#ae81ff">500m</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">limits</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">memory</span>: <span style="color:#ae81ff">500Mi</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ingress</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">readinessProbe</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>: <span style="color:#e6db74">&#34;/v1/sys/health?standbyok=true&amp;sealedcode=204&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">livenessProbe</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>: <span style="color:#e6db74">&#34;/v1/sys/health?standbyok=true&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">initialDelaySeconds</span>: <span style="color:#ae81ff">600</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tolerations</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">effect</span>: <span style="color:#ae81ff">NoSchedule</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">key</span>: <span style="color:#ae81ff">node-role.kubernetes.io/control-plane</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">Exists</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">nodeSelector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">homelab/role</span>: <span style="color:#e6db74">&#34;controller&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">networkPolicy</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">priorityClassName</span>: <span style="color:#e6db74">&#34;system-cluster-critical&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">extraLabels</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">homelab/app</span>: <span style="color:#ae81ff">vault</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">homelab/part-of</span>: <span style="color:#ae81ff">vault</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">service</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">active</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">standby</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#e6db74">&#34;LoadBalancer&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">externalTrafficPolicy</span>: <span style="color:#e6db74">&#34;Local&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">annotations</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">external-dns.alpha.kubernetes.io/hostname</span>: <span style="color:#ae81ff">newvault.example.com</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">io.cilium/lb-ipam-ips</span>: <span style="color:#ae81ff">300.300.300.12</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">includeConfigAnnotation</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">dataStorage</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">size</span>: <span style="color:#e6db74">&#34;1Gi&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">storageClass</span>: <span style="color:#ae81ff">rbd-fast</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">auditStorage</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">dev</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">extraVolumes</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">secret</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">name</span>: <span style="color:#ae81ff">vault-tls-certs</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">extraEnvironmentVars</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">VAULT_CACERT</span>: <span style="color:#e6db74">&#34;/vault/userconfig/vault-tls-certs/issuing_ca&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">standalone</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ha</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">raft</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">setNodeId</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">config</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        cluster_name = &#34;vault-k8s&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        ui = false
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        disable_mlock = false
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        listener &#34;tcp&#34; {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          address = &#34;[::]:8200&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          cluster_address = &#34;[::]:8201&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          tls_cert_file = &#34;/vault/userconfig/vault-tls-certs/certificate&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          tls_key_file  = &#34;/vault/userconfig/vault-tls-certs/private_key&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        }
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        storage &#34;raft&#34; {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          path = &#34;/vault/data&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          retry_join {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            leader_api_addr = &#34;https://vault-0.vault-internal:8200&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            leader_ca_cert_file = &#34;/vault/userconfig/vault-tls-certs/issuing_ca&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          }
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          retry_join {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            leader_api_addr = &#34;https://vault-1.vault-internal:8200&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            leader_ca_cert_file = &#34;/vault/userconfig/vault-tls-certs/issuing_ca&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          }
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          retry_join {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            leader_api_addr = &#34;https://vault-2.vault-internal:8200&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            leader_ca_cert_file = &#34;/vault/userconfig/vault-tls-certs/issuing_ca&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          }
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        }
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        service_registration &#34;kubernetes&#34; {}</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">replicas</span>: <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">ui</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">csi</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">serverTelemetry</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">serviceMonitor</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">false</span>
</span></span></code></pre></div><p>Let me explain. I&rsquo;m disabling the Ingress because I will make Vault accessible
via a LoadBalancer instead. There&rsquo;s no need to push it through Traefik, and
using it through Traefik would just mean one more service that needs to be up
and running before Vault is accessible.</p>
<p>Next, I&rsquo;m configuring the liveness probe. It needs to be reconfigured to make
sure that Vault also returns a <code>200</code> result when the pod being probed is in
standby. See also <a href="https://developer.hashicorp.com/vault/api-docs/system/health">these docs</a>.
And while setting Vault up, before initializing the cluster, just do yourself
the favor and completely disable the probes, or at least increase the <code>initialDelaySeconds</code>,
to prevent restarts while you&rsquo;re in the process of initializing the cluster.</p>
<p>Next, I&rsquo;m adding a toleration for <code>control-plane</code> nodes. This is mostly because
those are the only nodes with local storage, so they will be up first.</p>
<p>And then we come to the first problem with the chart, the service setup. In my
k8s cluster, I&rsquo;m using Cilium&rsquo;s BGP-based LoadBalancer support. And that requires
the Services Cilium looks at to have a specific, configurable label. But the
Vault Helm chart does not allow setting labels for the Services it creates.
Perhaps my use case is just really niche?
Anyway, I&rsquo;m enabling only the generic <code>vault</code> Service, set it to LoadBalancer
and, importantly, set the <code>externalTrafficPolicy</code> to <code>Local</code>. This means that
packets arriving for Vault will directly reach the node where the active Vault
Pod is running, instead of getting forwarded there by other nodes.
This is particularly important for Vault, because Vault can configure tokens
to be valid only when they&rsquo;re coming from certain IPs. This won&rsquo;t work when
the source IP looks like it&rsquo;s coming from another k8s node, instead of the actual
source host.
I&rsquo;m also setting a fixed IP to assign to the LoadBalancer, so I can easily set a
few choice firewall rules for access to that LoadBalancer.</p>
<p>After the Service configs follows the HA configuration. In this config, there can
be multiple Vault servers, continuously exchanging data. When the currently active
server goes down, another one can take over, and the previous leader goes into
the standby pool once its back.
Note that this is a high availability setup, not a load balancing setup. When
a request reaches a standby server, it is forwarded to the current active server.
The standby server never answers any requests, besides those to the health
endpoint, of course.
In my case, the HA config mostly consists of a config snippet for the Vault
config file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span>cluster_name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vault-k8s&#34;</span>
</span></span><span style="display:flex;"><span>ui <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>disable_mlock <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">listener</span> <span style="color:#e6db74">&#34;tcp&#34;</span> {
</span></span><span style="display:flex;"><span>  address <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;[::]:8200&#34;</span>
</span></span><span style="display:flex;"><span>  cluster_address <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;[::]:8201&#34;</span>
</span></span><span style="display:flex;"><span>  tls_cert_file <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/vault/userconfig/vault-tls-certs/certificate&#34;</span>
</span></span><span style="display:flex;"><span>  tls_key_file  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/vault/userconfig/vault-tls-certs/private_key&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">storage</span> <span style="color:#e6db74">&#34;raft&#34;</span> {
</span></span><span style="display:flex;"><span>  path <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/vault/data&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">retry_join</span> {
</span></span><span style="display:flex;"><span>    leader_api_addr <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://vault-0.vault-internal:8200&#34;</span>
</span></span><span style="display:flex;"><span>    leader_ca_cert_file <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/vault/userconfig/vault-tls-certs/issuing_ca&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">retry_join</span> {
</span></span><span style="display:flex;"><span>    leader_api_addr <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://vault-1.vault-internal:8200&#34;</span>
</span></span><span style="display:flex;"><span>    leader_ca_cert_file <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/vault/userconfig/vault-tls-certs/issuing_ca&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">retry_join</span> {
</span></span><span style="display:flex;"><span>    leader_api_addr <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://vault-2.vault-internal:8200&#34;</span>
</span></span><span style="display:flex;"><span>    leader_ca_cert_file <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/vault/userconfig/vault-tls-certs/issuing_ca&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">service_registration</span> <span style="color:#e6db74">&#34;kubernetes&#34;</span> {}
</span></span></code></pre></div><p>Most interesting here is the <code>retry_join</code> configuration, which needs to contain
the CA used to sign the TLS cert used in the <code>listener</code> stanza. I will explain
this more deeply in the next section, where I set up the cert generation.</p>
<p>Once that Helm chart gets deployed, a couple of things went wrong, leading to some
beautiful Yak shaving.</p>
<h2 id="setting-up-the-ciliumbgppeeringpolicy">Setting up the CiliumBGPPeeringPolicy</h2>
<p>As I&rsquo;ve noted above, the labels of the main Vault Service cannot be changed.
Interestingly, the two other services, for active and standby servers, do have
the option of configuring their labels. But not the main service.
Another issue: The type of the Services can only be set centrally, for all three
Services.</p>
<p>As you can read in a bit more detail <a href="https://blog.mei-home.net/posts/k8s-migration-2a-cilium-bgp/">here</a>,
I&rsquo;m using Cilium&rsquo;s BGP-based support for setting up LoadBalancer type services.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#e6db74">&#34;cilium.io/v2alpha1&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">CiliumBGPPeeringPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">worker-node-bgp</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">kube-system</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">nodeSelector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchExpressions</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">key</span>: <span style="color:#e6db74">&#34;homelab/role&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">In</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">values</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#e6db74">&#34;worker&#34;</span>
</span></span><span style="display:flex;"><span>          - <span style="color:#e6db74">&#34;ceph&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">virtualRouters</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">localASN</span>: <span style="color:#ae81ff">64555</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">exportPodCIDR</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">serviceSelector</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">matchExpressions</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">key</span>: <span style="color:#e6db74">&#34;homelab/public-service&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">In</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">values</span>:
</span></span><span style="display:flex;"><span>              - <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">neighbors</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">peerAddress</span>: <span style="color:#e6db74">&#39;300.300.300.405/32&#39;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">peerASN</span>: <span style="color:#ae81ff">64555</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">eBGPMultihopTTL</span>: <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">connectRetryTimeSeconds</span>: <span style="color:#ae81ff">120</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">holdTimeSeconds</span>: <span style="color:#ae81ff">90</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">keepAliveTimeSeconds</span>: <span style="color:#ae81ff">30</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">gracefulRestart</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">restartTimeSeconds</span>: <span style="color:#ae81ff">120</span>
</span></span></code></pre></div><p>The main problem here is the <code>spec.virtualRouters[0].serviceSelector</code>, as it
only allows matching on labels - and I cannot influence the labels set on the
Vault Service. I then took a very close look at the Cilium docs and found out
that the selector can also select on the Service name and namespace. So I
tried extending the above config like this, adding another entry in the
<code>virtualRouters</code> list:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#e6db74">&#34;cilium.io/v2alpha1&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">CiliumBGPPeeringPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">worker-node-bgp</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">kube-system</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">nodeSelector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchExpressions</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">key</span>: <span style="color:#e6db74">&#34;homelab/role&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">In</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">values</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#e6db74">&#34;worker&#34;</span>
</span></span><span style="display:flex;"><span>          - <span style="color:#e6db74">&#34;ceph&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">virtualRouters</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">localASN</span>: <span style="color:#ae81ff">64555</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">exportPodCIDR</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">serviceSelector</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">matchExpressions</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">key</span>: <span style="color:#e6db74">&#34;homelab/public-service&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">In</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">values</span>:
</span></span><span style="display:flex;"><span>              - <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">neighbors</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">peerAddress</span>: <span style="color:#e6db74">&#39;300.300.300.405/32&#39;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">peerASN</span>: <span style="color:#ae81ff">64555</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">eBGPMultihopTTL</span>: <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">connectRetryTimeSeconds</span>: <span style="color:#ae81ff">120</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">holdTimeSeconds</span>: <span style="color:#ae81ff">90</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">keepAliveTimeSeconds</span>: <span style="color:#ae81ff">30</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">gracefulRestart</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">restartTimeSeconds</span>: <span style="color:#ae81ff">120</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">localASN</span>: <span style="color:#ae81ff">64555</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">exportPodCIDR</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">serviceSelector</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">matchExpressions</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">key</span>: <span style="color:#e6db74">&#34;io.kubernetes.service.name&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">In</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">values</span>:
</span></span><span style="display:flex;"><span>              - <span style="color:#e6db74">&#34;vault&#34;</span>
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">key</span>: <span style="color:#e6db74">&#34;io.kubernetes.service.namespace&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">In</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">values</span>:
</span></span><span style="display:flex;"><span>              - <span style="color:#e6db74">&#34;vault&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">neighbors</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">peerAddress</span>: <span style="color:#e6db74">&#39;300.300.300.405/32&#39;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">peerASN</span>: <span style="color:#ae81ff">64555</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">eBGPMultihopTTL</span>: <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">connectRetryTimeSeconds</span>: <span style="color:#ae81ff">120</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">holdTimeSeconds</span>: <span style="color:#ae81ff">90</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">keepAliveTimeSeconds</span>: <span style="color:#ae81ff">30</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">gracefulRestart</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">restartTimeSeconds</span>: <span style="color:#ae81ff">120</span>
</span></span></code></pre></div><p>But, this did not work at all. Cilium announced either only the Services which
matched the first <code>serviceSelector</code> or the ones matching the second, but never
both. When debugging this issue, you can use <code>cilium bgp routes</code> to show which
routes cilium advertises to neighbors.</p>
<p>What did end up working was to introduce two peering policies. The hosts they
apply to seem to also have to avoid any overlap, or it again won&rsquo;t work.
I&rsquo;ve got it configured like this now:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#e6db74">&#34;cilium.io/v2alpha1&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">CiliumBGPPeeringPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">worker-node-bgp</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">kube-system</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">nodeSelector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchExpressions</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">key</span>: <span style="color:#e6db74">&#34;homelab/role&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">In</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">values</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#e6db74">&#34;worker&#34;</span>
</span></span><span style="display:flex;"><span>          - <span style="color:#e6db74">&#34;ceph&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">virtualRouters</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">localASN</span>: <span style="color:#ae81ff">64555</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">exportPodCIDR</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">serviceSelector</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">matchExpressions</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">key</span>: <span style="color:#e6db74">&#34;homelab/public-service&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">In</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">values</span>:
</span></span><span style="display:flex;"><span>              - <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">neighbors</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">peerAddress</span>: <span style="color:#e6db74">&#39;300.300.300.405/32&#39;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">peerASN</span>: <span style="color:#ae81ff">64555</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">eBGPMultihopTTL</span>: <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">connectRetryTimeSeconds</span>: <span style="color:#ae81ff">120</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">holdTimeSeconds</span>: <span style="color:#ae81ff">90</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">keepAliveTimeSeconds</span>: <span style="color:#ae81ff">30</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">gracefulRestart</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">restartTimeSeconds</span>: <span style="color:#ae81ff">120</span>
</span></span><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#e6db74">&#34;cilium.io/v2alpha1&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">CiliumBGPPeeringPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">controller-node-bgp</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">kube-system</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">nodeSelector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchExpressions</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">key</span>: <span style="color:#e6db74">&#34;homelab/role&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">In</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">values</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#e6db74">&#34;controller&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">virtualRouters</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">localASN</span>: <span style="color:#ae81ff">64555</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">exportPodCIDR</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">serviceSelector</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">matchExpressions</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">key</span>: <span style="color:#e6db74">&#34;io.kubernetes.service.name&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">In</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">values</span>:
</span></span><span style="display:flex;"><span>              - <span style="color:#e6db74">&#34;vault&#34;</span>
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">key</span>: <span style="color:#e6db74">&#34;io.kubernetes.service.namespace&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">In</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">values</span>:
</span></span><span style="display:flex;"><span>              - <span style="color:#e6db74">&#34;vault&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">neighbors</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">peerAddress</span>: <span style="color:#e6db74">&#39;300.300.300.405/32&#39;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">peerASN</span>: <span style="color:#ae81ff">64555</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">eBGPMultihopTTL</span>: <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">connectRetryTimeSeconds</span>: <span style="color:#ae81ff">120</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">holdTimeSeconds</span>: <span style="color:#ae81ff">90</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">keepAliveTimeSeconds</span>: <span style="color:#ae81ff">30</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">gracefulRestart</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">enabled</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">restartTimeSeconds</span>: <span style="color:#ae81ff">120</span>
</span></span></code></pre></div><p>So one policy does the normal thing, for all of the Services where I can control
the labels, running on my Ceph and worker hosts.
Then there is the second policy, which only applies to the <code>vault</code> service in
the <code>vault</code> namespace. With that configuration, I got Cilium to announce both
of them.</p>
<h2 id="setting-up-the-certificates-for-vault">Setting up the certificates for Vault</h2>
<p>The next step, accounting for the majority of preparation work, was the certificate
setup. The Vault Pods need to be able to access each other, and they should do
it over HTTPS. So they need certificates. Initially, I did not realize that and
naively just told Vault to use my Let&rsquo;s Encrypt external cert. But of course
the Vault instances need to contact each other directly, not just the active
server available via the <code>newvault.example.com</code> address.</p>
<p>So I needed a specific certificate for Vault, with the following three SANs:</p>
<ul>
<li><code>vault-0.vault-internal</code></li>
<li><code>vault-1.vault-internal</code></li>
<li><code>vault-2.vault-internal</code></li>
</ul>
<p>And as I noted above, I&rsquo;ve already got an internal CA. And here is where I
knowingly committed a sin: That CA is provided by Vault, via the
<a href="https://developer.hashicorp.com/vault/docs/secrets/pki">PKI secrets engine</a>.
And I reused that CA for generating the Vault CA. Knowingly introducing a
dependency cycle into my setup. I feel a bit ashamed for it. But I also don&rsquo;t
want to introduce another complete PKI setup. And reusing the already existing
CA has the benefit that the CA cert is already widely deployed in my Homelab.</p>
<p>The first step is to set up a separate <a href="https://developer.hashicorp.com/vault/docs/secrets/pki/quick-start-intermediate-ca#configure-a-role">role</a>
for the certificate, so I can properly separate access to generating certificates
later.</p>
<p>I&rsquo;ve got all of my Vault configuration in Terraform, so I added the new role
there as well. Here is the full CA setup:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;vault_mount&#34; &#34;my-ca-mount&#34;</span> {
</span></span><span style="display:flex;"><span>  path <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;my-ca&#34;</span>
</span></span><span style="display:flex;"><span>  type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;pki&#34;</span>
</span></span><span style="display:flex;"><span>  default_lease_ttl_seconds <span style="color:#f92672">=</span> <span style="color:#ae81ff">157680000</span>
</span></span><span style="display:flex;"><span>  max_lease_ttl_seconds <span style="color:#f92672">=</span> <span style="color:#ae81ff">157680000</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;vault_pki_secret_backend_root_cert&#34; &#34;my-root-cert&#34;</span> {
</span></span><span style="display:flex;"><span>  backend <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault_mount</span>.<span style="color:#66d9ef">my</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">ca</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">mount</span>.<span style="color:#66d9ef">path</span>
</span></span><span style="display:flex;"><span>  type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;internal&#34;</span>
</span></span><span style="display:flex;"><span>  common_name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;My Private Root CA&#34;</span>
</span></span><span style="display:flex;"><span>  ttl <span style="color:#f92672">=</span> <span style="color:#ae81ff">157680000</span>
</span></span><span style="display:flex;"><span>  format <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;pem&#34;</span>
</span></span><span style="display:flex;"><span>  private_key_format <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;der&#34;</span>
</span></span><span style="display:flex;"><span>  key_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;rsa&#34;</span>
</span></span><span style="display:flex;"><span>  key_bits <span style="color:#f92672">=</span> <span style="color:#ae81ff">4096</span>
</span></span><span style="display:flex;"><span>  exclude_cn_from_sans <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  ou <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Private&#34;</span>
</span></span><span style="display:flex;"><span>  organization <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Private&#34;</span>
</span></span><span style="display:flex;"><span>  country <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;DE&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;vault_pki_secret_backend_config_urls&#34; &#34;my-root-urls&#34;</span> {
</span></span><span style="display:flex;"><span>  backend <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault_mount</span>.<span style="color:#66d9ef">my</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">ca</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">mount</span>.<span style="color:#66d9ef">path</span>
</span></span><span style="display:flex;"><span>  issuing_certificates <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;https://vault.example.com/v1/my-ca/ca&#34;</span>
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;vault_pki_secret_backend_role&#34; &#34;vault-certs&#34;</span> {
</span></span><span style="display:flex;"><span>  backend <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault_mount</span>.<span style="color:#66d9ef">my</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">ca</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">mount</span>.<span style="color:#66d9ef">path</span>
</span></span><span style="display:flex;"><span>  name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vault-certs&#34;</span>
</span></span><span style="display:flex;"><span>  ttl <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;15552000&#34;</span>
</span></span><span style="display:flex;"><span>  max_ttl <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;15552000&#34;</span>
</span></span><span style="display:flex;"><span>  allow_localhost <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  allowed_domains <span style="color:#f92672">=</span> [ <span style="color:#e6db74">&#34;newvault.example.com&#34;, &#34;vault-internal&#34;, &#34;127.0.0.1&#34;</span> ]
</span></span><span style="display:flex;"><span>  allow_subdomains <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  allow_ip_sans <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  allow_wildcard_certificates <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  allow_bare_domains <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  key_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;rsa&#34;</span>
</span></span><span style="display:flex;"><span>  key_bits <span style="color:#f92672">=</span> <span style="color:#ae81ff">4096</span>
</span></span><span style="display:flex;"><span>  organization <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;My Homelab&#34;</span>]
</span></span><span style="display:flex;"><span>  country <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;DE&#34;</span>]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>There&rsquo;s also another role for certs deployed for other purposes, but that&rsquo;s not
important here. I will not go over the base CA and mount configuration and
instead concentrate on the <code>vault-certs</code> role.</p>
<p>This role allows creating certificates for the <code>vault-internal</code> domain, covering
the three Pod&rsquo;s internal DNS names, the externally visible address of the Vault
cluster that the LoadBalancer points to at <code>newvault.example.com</code> and the
localhost address. The localhost IP is there because that&rsquo;s how the <code>vault</code> CLI
launched inside the Pods contacts the local Vault instance. That use case is
required for initialization and unsealing later.</p>
<p>This is the beauty of Terraform at work again. I could of course also do the
same using the Vault CLI, but then I would need to document the commands somewhere
and remember to update that documentation whenever something changes. With the
Infrastructure as code approach, I would make any changes in this definition,
so it&rsquo;s always up-to-date.</p>
<p>After a <code>terraform apply</code>, I could produce a certificate with this command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>vault write -format<span style="color:#f92672">=</span>json my-ca/issue/vault-certs common_name<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;newvault.example.com&#34;</span> alt_names<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;vault-0.vault-internal,vault-1.vault-internal,vault-2.vault-internal&#34;</span> ttl<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;4000h&#34;</span> &gt; test.cert
</span></span></code></pre></div><p>The JSON file that produces looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;request_id&#34;</span>: <span style="color:#e6db74">&#34;5d72d050-8f80-ba4f-1067-b4165cf2d0f5&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;lease_id&#34;</span>: <span style="color:#e6db74">&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;lease_duration&#34;</span>: <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;renewable&#34;</span>: <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;data&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;ca_chain&#34;</span>: [
</span></span><span style="display:flex;"><span>      <span style="color:#e6db74">&#34;-----BEGIN CERTIFICATE-----\n[...]\n-----END CERTIFICATE-----&#34;</span>
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;certificate&#34;</span>: <span style="color:#e6db74">&#34;-----BEGIN CERTIFICATE-----\n [...] \n-----END CERTIFICATE-----&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;expiration&#34;</span>: <span style="color:#ae81ff">1757892246</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;issuing_ca&#34;</span>: <span style="color:#e6db74">&#34;-----BEGIN CERTIFICATE-----\n [...] \n-----END CERTIFICATE-----&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;private_key&#34;</span>: <span style="color:#e6db74">&#34;-----BEGIN RSA PRIVATE KEY-----\n[...]\n-----END RSA PRIVATE KEY-----&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;private_key_type&#34;</span>: <span style="color:#e6db74">&#34;rsa&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;serial_number&#34;</span>: <span style="color:#e6db74">&#34;7d:fe:7e:97:c1:56:96:eb:3d:27:e8:ee:48:78:82:bd:ca:f8:0d:7e&#34;</span>
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;mount_type&#34;</span>: <span style="color:#e6db74">&#34;pki&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>And I then revoked this test cert using the <code>serial_number</code> with this command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>vault write my-ca/revoke serial_number<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;7d:fe:7e:97:c1:56:96:eb:3d:27:e8:ee:48:78:82:bd:ca:f8:0d:7e&#34;</span>
</span></span></code></pre></div><h3 id="getting-the-certificate-into-kubernetes">Getting the certificate into Kubernetes</h3>
<p>I could then of course just upload the key and certificate into a k8s Secret,
but that just doesn&rsquo;t feel very Kubernetes-y, plus it would be a step I would
need to document for future renewals. Instead, I had another look at external-secrets
and found the <a href="https://external-secrets.io/latest/api/generator/vault/">VaultDynamicSecret</a>.</p>
<p>This is another nice feature for getting Vault outputs into k8s Secrets, only
this time it&rsquo;s not static credentials, but a certificate, complete with automatic
renewal. And the usage of the PKI secrets engine is even the example used in the
docs.</p>
<p>I initially deployed a manifest that looked like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">generators.external-secrets.io/v1alpha1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">VaultDynamicSecret</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;vault-certs-generator&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">path</span>: <span style="color:#e6db74">&#34;my-ca/issue/vault-certs&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">method</span>: <span style="color:#e6db74">&#34;POST&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">parameters</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">common_name</span>: <span style="color:#e6db74">&#34;newvault.example.com&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">alt_names</span>: <span style="color:#e6db74">&#34;vault-0.vault-internal,vault-1.vault-internal,vault-2.vault-internal&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ip_sans</span>: <span style="color:#e6db74">&#34;127.0.0.1&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">resultType</span>: <span style="color:#e6db74">&#34;Data&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">provider</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">server</span>: <span style="color:#e6db74">&#34;https://vault.example.com:8200&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">caProvider</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">type</span>: <span style="color:#ae81ff">Secret</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">name</span>: <span style="color:#ae81ff">homelab-ca-cert</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">namespace</span>: {{ <span style="color:#ae81ff">.Release.Namespace }}</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">key</span>: <span style="color:#ae81ff">caCert</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">auth</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">appRole</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">path</span>: <span style="color:#e6db74">&#34;approle&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">roleId</span>: {{ <span style="color:#ae81ff">.Values.approleId }}</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">secretRef</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;external-secrets-approle&#34;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">namespace</span>: {{ <span style="color:#ae81ff">.Release.Namespace }}</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">key</span>: <span style="color:#e6db74">&#34;secretId&#34;</span>
</span></span></code></pre></div><p>I deployed this manifest in the external-secret&rsquo;s namespace, because that was
where the <a href="https://developer.hashicorp.com/vault/docs/auth/approle">AppRole</a>
auth secrets lived.</p>
<p>Then I created the following ExternalSecret in the <code>vault</code> namespace to generate
a certificate:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">external-secrets.io/v1beta1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ExternalSecret</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;vault-tls-certs&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">refreshInterval</span>: <span style="color:#e6db74">&#34;4000h&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">target</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">vault-tls-certs</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">dataFrom</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">sourceRef</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">generatorRef</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">generators.external-secrets.io/v1alpha1</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">kind</span>: <span style="color:#ae81ff">VaultDynamicSecret</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;vault-certs-generator&#34;</span>
</span></span></code></pre></div><p>This didn&rsquo;t work, and I got an error about the <code>vault-certs-generator</code> not
being found. This was because the non-Cluster variants of external-secrets
objects are generally only available in the namespace where they were created.
So my ExternalSecret in the <code>vault</code> namespace wasn&rsquo;t able to access the
VaultDynamicSecret in the external-secrets namespace.</p>
<p>So I ended up moving the ExternalSecret into the external-secrets namespace
as well, just to make sure that it even works. That introduced me to an
authorization error looking something like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{<span style="color:#f92672">&#34;level&#34;</span>:<span style="color:#e6db74">&#34;error&#34;</span>,
</span></span><span style="display:flex;"><span><span style="color:#f92672">&#34;ts&#34;</span>:<span style="color:#ae81ff">1742423406.4829333</span>,
</span></span><span style="display:flex;"><span><span style="color:#f92672">&#34;msg&#34;</span>:<span style="color:#e6db74">&#34;Reconciler error&#34;</span>,
</span></span><span style="display:flex;"><span><span style="color:#f92672">&#34;controller&#34;</span>:<span style="color:#e6db74">&#34;externalsecret&#34;</span>,
</span></span><span style="display:flex;"><span><span style="color:#f92672">&#34;controllerGroup&#34;</span>:<span style="color:#e6db74">&#34;external-secrets.io&#34;</span>,
</span></span><span style="display:flex;"><span><span style="color:#f92672">&#34;controllerKind&#34;</span>:<span style="color:#e6db74">&#34;ExternalSecret&#34;</span>,
</span></span><span style="display:flex;"><span><span style="color:#f92672">&#34;ExternalSecret&#34;</span>:{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;name&#34;</span>:<span style="color:#e6db74">&#34;vault-tls-certs&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;namespace&#34;</span>:<span style="color:#e6db74">&#34;external-secrets&#34;</span>
</span></span><span style="display:flex;"><span>},
</span></span><span style="display:flex;"><span><span style="color:#f92672">&#34;namespace&#34;</span>:<span style="color:#e6db74">&#34;external-secrets&#34;</span>,
</span></span><span style="display:flex;"><span><span style="color:#f92672">&#34;name&#34;</span>:<span style="color:#e6db74">&#34;vault-tls-certs&#34;</span>,
</span></span><span style="display:flex;"><span><span style="color:#f92672">&#34;reconcileID&#34;</span>:<span style="color:#e6db74">&#34;d6b0b369-f959-479a-8228-f9a8d6fbc5bd&#34;</span>,
</span></span><span style="display:flex;"><span><span style="color:#f92672">&#34;error&#34;</span>:<span style="color:#e6db74">&#34;error processing spec.dataFrom[0].sourceRef.generatorRef,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">err: error using generator: Error making API request.\n\nURL:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">PUT https://vault.example.com:8200/v1/my-ca/issue/vault-certs
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Code: 403. Errors:\n\n* 1 error occurred:\n\t* permission denied\n\n&#34;</span>,
</span></span><span style="display:flex;"><span><span style="color:#f92672">&#34;stacktrace&#34;</span>:<span style="color:#e6db74">&#34;...&#34;</span>}
</span></span></code></pre></div><p>This was due to a mistake I had made in updating the policy for external-secrets
to allow it access to the <code>my-ca/issue/vault-certs</code> endpoint. The policy addition
I had made for that particular path looked like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">path</span> <span style="color:#e6db74">&#34;my-ca/issue/vault-certs&#34;</span> {
</span></span><span style="display:flex;"><span>  capabilities <span style="color:#f92672">=</span> [ <span style="color:#e6db74">&#34;create&#34;</span> ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That&rsquo;s what all the examples I could find said. But it did not work. I finally
added all permissions and then slowly removed the capabilities one by one,
until I arrived at only the <code>update</code> capability missing from the above.</p>
<p>After fixing that I finally got a certificate created. But I still had to do
something about the fact that the ExternalSecret lived in the external-secrets
namespace now, while it was needed in Vault&rsquo;s namespace.</p>
<p>One option I looked at to resolve this issue is <a href="https://external-secrets.io/latest/api/generator/cluster/">ClusterGenerators</a>.
These work similar to namespaced generators like VaultDynamicSecret, but allow
usage in ExternalSecrets throughout the cluster.
I ended up deciding against that, for simple &ldquo;doing things properly&rdquo; reasons:
The generator will only ever be needed in the Vault namespace, because it is not
a generic generator for TLS certs, but a specific generator restricted to creating
certs for Vault.</p>
<p>So I decided to stay with the namespaced VaultDynamicSecret, but change the
auth method to Kubernetes.</p>
<h3 id="setting-up-kubernetes-auth">Setting up Kubernetes auth</h3>
<p>Being the Swiss army knife that it is, Vault can also authenticate with
<a href="https://developer.hashicorp.com/vault/docs/auth/kubernetes">Kubernetes</a>.
The way this works is that you can create a role in Vault and assign policies
defining what that role can do. Then, certain Kubernetes ServiceAccounts can be
allowed to authenticate with that role. Vault then expects to receive a Kubernetes
JWT token to verify the authentication, which it then contacts Kubernetes for
to ensure the token is valid and belongs to one of the ServiceAccounts allowed
to use the Vault role.</p>
<p>One problem is that the action of verifying Kubernetes tokens itself also needs
a Kubernetes token for the API server access. With a Vault deployed via the
Helm chart that&rsquo;s easy, the <code>vault</code> ServiceAccount created by the chart already
has the necessary permissions, and Vault can use that account&rsquo;s token.
Vault will also automatically reload the token periodically, as Kubernetes
tokens are generally short-lived.</p>
<p>But at least initially, I need to use my baremetal Vault for the certificate
generation, because those are the certs that the k8s Vault deployment will use
later. To work around this issue, one could still use long-lived tokens. But
another way would be to use the JWT of the process that&rsquo;s trying to use the
Vault auth method. This requires some changes in Kubernetes though. Namely, the
ServiceAccounts which should validate to Vault via Kubernetes auth need to have
the <code>system:auth-delegator</code> ClusterRole. This allows the ServiceAccount&rsquo;s token
to be used by other apps (here, Vault) to authenticate with that token. Vault
can use this to access the <a href="https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/">TokenReview</a>
API to verify that the token is valid.
No change is necessary here, because the <code>vault</code> ServiceAccount I will be using
already has the <code>auth-delegator</code> role.</p>
<p>So with that out of the way, here is the Vault Kubernetes auth setup:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;vault_auth_backend&#34; &#34;kubernetes&#34;</span> {
</span></span><span style="display:flex;"><span>  type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;kubernetes&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;vault_kubernetes_auth_backend_config&#34; &#34;kube-backend-config&#34;</span> {
</span></span><span style="display:flex;"><span>  backend                <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault_auth_backend</span>.<span style="color:#66d9ef">kubernetes</span>.<span style="color:#66d9ef">path</span>
</span></span><span style="display:flex;"><span>  kubernetes_host        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://k8s.exmaple.com:6443&#34;</span>
</span></span><span style="display:flex;"><span>  issuer                 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;api&#34;</span>
</span></span><span style="display:flex;"><span>  disable_iss_validation <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>  kubernetes_ca_cert               <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;-----BEGIN CERTIFICATE-----\n [...]&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;vault_kubernetes_auth_backend_role&#34; &#34;vault-certs&#34;</span> {
</span></span><span style="display:flex;"><span>  backend                          <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault_auth_backend</span>.<span style="color:#66d9ef">kubernetes</span>.<span style="color:#66d9ef">path</span>
</span></span><span style="display:flex;"><span>  role_name                        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vault-certs&#34;</span>
</span></span><span style="display:flex;"><span>  bound_service_account_names      <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;vault&#34;</span>]
</span></span><span style="display:flex;"><span>  bound_service_account_namespaces <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;vault&#34;</span>]
</span></span><span style="display:flex;"><span>  token_ttl                        <span style="color:#f92672">=</span> <span style="color:#ae81ff">3600</span>
</span></span><span style="display:flex;"><span>  token_policies                   <span style="color:#f92672">=</span> [<span style="color:#66d9ef">vault_policy</span>.<span style="color:#66d9ef">vault</span><span style="color:#960050;background-color:#1e0010">-</span><span style="color:#66d9ef">certs</span>.<span style="color:#66d9ef">name</span>]
</span></span><span style="display:flex;"><span>  token_bound_cidrs                <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;300.300.300.0/24&#34;</span>]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>In this setup, the Kubernetes auth is created for the k8s API at <code>k8s.example.com:6443</code>.
The general k8s info can be found via this command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>kubectl cluster-info
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Kubernetes control plane is running at https://k8s.example.com:6443
</span></span><span style="display:flex;"><span>CoreDNS is running at https://k8s.example.com:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>To further debug and diagnose cluster problems, use <span style="color:#e6db74">&#39;kubectl cluster-info dump&#39;</span>.
</span></span></code></pre></div><p>To get the <code>kubernetes_ca_cert</code>, you can have a look at the <code>kube-root-ca.crt</code>
ConfigMap that should be available in all namespaces, for example like this:</p>
<pre tabindex="0"><code>kubectl get -n kube-system configmaps kube-root-ca.crt -o jsonpath=&#34;{[&#39;data&#39;][&#39;ca\.crt&#39;]}&#34;
</code></pre><p>Finally, I&rsquo;Ve also restricted all tokens created by the <code>vault-certs</code> role so
that they&rsquo;re only valid coming from IPs in the Homelab. That&rsquo;s just a small
defense in depth method I like to apply for any tokens in Vault where it&rsquo;s possible.</p>
<h3 id="finally-setting-up-the-certificate-generator">Finally setting up the certificate generator</h3>
<p>With the authentication now configured properly, the certificate generation
can be set up like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">external-secrets.io/v1beta1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ExternalSecret</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;vault-tls-certs&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">refreshInterval</span>: <span style="color:#e6db74">&#34;4000h&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">target</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">vault-tls-certs</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">dataFrom</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">sourceRef</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">generatorRef</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">generators.external-secrets.io/v1alpha1</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">kind</span>: <span style="color:#ae81ff">VaultDynamicSecret</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;vault-certs-generator&#34;</span>
</span></span><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">generators.external-secrets.io/v1alpha1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">VaultDynamicSecret</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;vault-certs-generator&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">path</span>: <span style="color:#e6db74">&#34;my-ca/issue/vault-certs&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">method</span>: <span style="color:#e6db74">&#34;POST&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">parameters</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">common_name</span>: <span style="color:#e6db74">&#34;newvault.example.com&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">alt_names</span>: <span style="color:#e6db74">&#34;vault-0.vault-internal,vault-1.vault-internal,vault-2.vault-internal&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ip_sans</span>: <span style="color:#e6db74">&#34;127.0.0.1&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">resultType</span>: <span style="color:#e6db74">&#34;Data&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">provider</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">server</span>: <span style="color:#e6db74">&#34;https://vault.example.com:8200&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">caProvider</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">type</span>: <span style="color:#ae81ff">Secret</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">name</span>: <span style="color:#ae81ff">homelab-ca-cert</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">vault</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">key</span>: <span style="color:#ae81ff">caCert</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">auth</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">kubernetes</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">mountPath</span>: <span style="color:#e6db74">&#34;kubernetes&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">role</span>: <span style="color:#e6db74">&#34;vault-certs&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">serviceAccountRef</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;vault&#34;</span>
</span></span></code></pre></div><p>With this configuration, the Vault certs are collected (still from the old
baremetal Vault), with the Kubernetes authentication using the <code>vault</code> ServiceAccount.
This now works without issue, and a certificate usable by the k8s Vault instance
is generated.</p>
<p>I&rsquo;m also setting the renewal time of the Secret containing the certificate to
4000 hours. This should lead to automatic renewal with quite some time to spare,
as the certificates are given a lifetime of 4400h.</p>
<p>One thing to note is that the VaultDynamicSecret also needs the CA certificate.
The way I&rsquo;m currently supplying that one is a bit hacky. I&rsquo;m deploying Vault
with a Helm chart, and I&rsquo;ve added this to the <code>values.yaml</code> file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">caBundle</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  {{- exec &#34;curl&#34; (list &#34;https://vault.example.com:8200/v1/my-ca/ca/pem&#34;) | nindent 2 }}</span>
</span></span></code></pre></div><p>This is a special functionality of the tool I&rsquo;ve been using to manage all of
the Helm charts in my cluster, <a href="https://github.com/helmfile/helmfile">Helmfile</a>.
It can interpret Go templates in the <code>values.yaml</code> file. That line fetches the
CA certificate from the Vault endpoint and stores it in the <code>caBundle</code> variable.
That is then used to create a Secret with the CA like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Secret</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">homelab-ca-cert</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">stringData</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">caCert</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    {{- .Values.caBundle | nindent 6 }}</span>
</span></span></code></pre></div><h2 id="initializing-vault">Initializing Vault</h2>
<p>With all those Yaks safely shaven, I could finally go forward with initializing
the Kubernetes Vault cluster.</p>
<p>I used this command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>kubectl exec -n vault vault-0 -- vault operator init -key-shares<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span> -key-threshold<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span> &gt; vault-init.txt
</span></span></code></pre></div><p>This initialization failed with a certificate error:</p>
<pre tabindex="0"><code>Get &#34;https://127.0.0.1:8200/v1/sys/seal-status&#34;: tls: failed to verify certificate: x509: cannot validate certificate for 127.0.0.1 because it doesn&#39;t contain any IP SANs
</code></pre><p>Even for local connections, Vault needs a cert. And that&rsquo;s why I&rsquo;ve got the
<code>127.0.0.1</code> IP SAN in the certificate used by Vault.</p>
<p>After I got that issue fixed, I finally successfully initialized the Vault
instance, resulting in this information:</p>
<pre tabindex="0"><code>Unseal Key 1: abcde123

Initial Root Token: hvs.foobar

Vault initialized with 1 key shares and a key threshold of 1. Please securely
distribute the key shares printed above. When the Vault is re-sealed,
restarted, or stopped, you must supply at least 1 of these keys to unseal it
before it can start servicing requests.

Vault does not store the generated root key. Without at least 1 keys to
reconstruct the root key, Vault will remain permanently sealed!

It is possible to generate new unseal keys, provided you have a quorum of
existing unseal keys shares. See &#34;vault operator rekey&#34; for more information.
</code></pre><p>In the initialization command, I told Vault that I only need one key share.
Normally, you would split the key into multiple shares so they can be distributed,
but that doesn&rsquo;t make any real sense for a small personal instance. If somebody
somehow gets one of the key shares, they would very likely be able to get the
others the same way.</p>
<p>It is very important to save the initial root token <code>hvs.foobar</code>. This is
needed for the initial configuration, until some policies and other auth
methods have been configured.</p>
<p>The next step was then to unseal all three Vault instances with these commands
and the unseal key output by the <code>vault init</code> command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>kubectl exec -it -n vault vault-0 -- vault operator unseal
</span></span><span style="display:flex;"><span>kubectl exec -it -n vault vault-1 -- vault operator unseal
</span></span><span style="display:flex;"><span>kubectl exec -it -n vault vault-2 -- vault operator unseal
</span></span></code></pre></div><p>One interesting thing to note: All of the Vault Pods, as configured by the
Helm chart, run with the OnDelete update strategy. This has the effect that no
change to the configuration, including e.g. setting new environment variables,
will do anything. The Pods always need to be deleted manually to make a change.</p>
<h2 id="configuring-vault-logging">Configuring Vault logging</h2>
<p>I like having my logs all in at least approximately the same format, and so
I&rsquo;ve got a log parsing section for most apps in my FluentD config. Normally I
don&rsquo;t mention this, but Vault is a little bit weird. Namely, it does output
its logs as JSON if so configured, which is good. It makes parsing a lot simpler.
But, it also adds an <code>@</code> symbol to the names of <em>most</em> of the JSON keys:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{<span style="color:#f92672">&#34;@level&#34;</span>:<span style="color:#e6db74">&#34;info&#34;</span>,<span style="color:#f92672">&#34;@message&#34;</span>:<span style="color:#e6db74">&#34;compacting logs&#34;</span>,<span style="color:#f92672">&#34;@module&#34;</span>:<span style="color:#e6db74">&#34;storage.raft&#34;</span>,<span style="color:#f92672">&#34;@timestamp&#34;</span>:<span style="color:#e6db74">&#34;2025-04-06T18:11:31.197956Z&#34;</span>,<span style="color:#f92672">&#34;from&#34;</span>:<span style="color:#ae81ff">893122</span>,<span style="color:#f92672">&#34;to&#34;</span>:<span style="color:#ae81ff">901345</span>}
</span></span><span style="display:flex;"><span>{<span style="color:#f92672">&#34;@level&#34;</span>:<span style="color:#e6db74">&#34;info&#34;</span>,<span style="color:#f92672">&#34;@message&#34;</span>:<span style="color:#e6db74">&#34;snapshot complete up to&#34;</span>,<span style="color:#f92672">&#34;@module&#34;</span>:<span style="color:#e6db74">&#34;storage.raft&#34;</span>,<span style="color:#f92672">&#34;@timestamp&#34;</span>:<span style="color:#e6db74">&#34;2025-04-06T18:11:31.235460Z&#34;</span>,<span style="color:#f92672">&#34;index&#34;</span>:<span style="color:#ae81ff">911585</span>}
</span></span></code></pre></div><p>And I&rsquo;ve got no idea why. Or how it decides which keys get the <code>@</code> and which
do not. It made my log parsing a little bit more complicated. It now looks
like this:</p>
<pre tabindex="0"><code># Log config for the Vault deployment
&lt;filter services.vault.vault&gt;
  @type parser
  key_name log
  reserve_data true
  remove_key_name_field true
  &lt;parse&gt;
    @type multi_format
    &lt;pattern&gt;
      format json
      time_key &#34;@timestamp&#34;
      time_type string
      time_format %iso8601
      utc true
    &lt;/pattern&gt;
    &lt;pattern&gt;
      format regexp
      expression /^(?&lt;msg&gt;.*)$/
      time_key nil
    &lt;/pattern&gt;
  &lt;/parse&gt;
&lt;/filter&gt;

&lt;filter services.vault.vault&gt;
  @type record_modifier
  remove_keys _dummy_,@level
  &lt;record&gt;
    _dummy_ ${record[&#34;level&#34;] = record[&#34;@level&#34;] if record.key?(&#34;@level&#34;)}
  &lt;/record&gt;
&lt;/filter&gt;
</code></pre><p>The first filter does the main parsing, while the second one specifically
removes the <code>@</code> in front of the <code>level</code> entry in the log object, because
that&rsquo;s the key where my setup expects to see the log level.</p>
<p>Another weird thing, where Vault is by far not the biggest offender, are apps
which log in multiple different formats. That&rsquo;s why the first filter has a
<code>multi_format</code> parser. For reasons I&rsquo;m not sure about, Vault outputs some general
information in the beginning of the log, during startup, where it doesn&rsquo;t respect
the log format configuration:</p>
<pre tabindex="0"><code>==&gt; Vault server configuration:

Administrative Namespace:
             Api Address: https://10.8.1.61:8200
                     Cgo: disabled
         Cluster Address: https://vault-0.vault-internal:8201
   Environment Variables: HOME, HOSTNAME, HOST_IP, KUBERNETES_PORT, KUBERNETES_PORT_443_TCP, KUBERNETES_PORT_443_TCP_ADDR, KUBERNETES_PORT_443_TCP_PORT, KUBERNETES_PORT_443_TCP_PROTO, KUBERNETES_SERVICE_HOST, KUBERNETES_SERVICE_PORT, KUBERNETES_SERVICE_PORT_HTTPS, NAME, PATH, POD_IP, PWD, SHLVL, SKIP_CHOWN, SKIP_SETCAP, TERM, VAULT_ADDR, VAULT_API_ADDR, VAULT_CACERT, VAULT_CLUSTER_ADDR, VAULT_K8S_NAMESPACE, VAULT_K8S_POD_NAME, VAULT_LOG_FORMAT, VAULT_LOG_LEVEL, VAULT_PORT, VAULT_PORT_8200_TCP, VAULT_PORT_8200_TCP_ADDR, VAULT_PORT_8200_TCP_PORT, VAULT_PORT_8200_TCP_PROTO, VAULT_PORT_8201_TCP, VAULT_PORT_8201_TCP_ADDR, VAULT_PORT_8201_TCP_PORT, VAULT_PORT_8201_TCP_PROTO, VAULT_RAFT_NODE_ID, VAULT_SERVICE_HOST, VAULT_SERVICE_PORT, VAULT_SERVICE_PORT_HTTPS, VAULT_SERVICE_PORT_HTTPS_INTERNAL, VERSION
              Go Version: go1.23.6
              Listener 1: tcp (addr: &#34;[::]:8200&#34;, cluster address: &#34;[::]:8201&#34;, disable_request_limiter: &#34;false&#34;, max_request_duration: &#34;1m30s&#34;, max_request_size: &#34;33554432&#34;, tls: &#34;enabled&#34;)
               Log Level: debug
                   Mlock: supported: true, enabled: false
           Recovery Mode: false
                 Storage: raft (HA available)
                 Version: Vault v1.18.5, built 2025-02-24T09:40:28Z
             Version Sha: 2cb3755273dbd63f5b0f8ec50089b57ffd3fa330

==&gt; Vault server started! Log data will stream in below:
</code></pre><p>Why output that in plain text, instead of also putting it into JSON? It seems to
be a quirk of all of HashiCorp&rsquo;s tools, Nomad and Consul also do the same thing
if I remember correctly.</p>
<h2 id="migrating-to-the-new-vault-instance">Migrating to the new Vault instance</h2>
<p>With the instance on Kubernetes now configured, I need to migrate the data to
that instance. Sadly, there&rsquo;s not really a good way to migrate especially K/V
store entries from one Vault to another. So I just went with manual migration.</p>
<h3 id="running-terraform-against-the-new-instance">Running Terraform against the new instance</h3>
<p>As I&rsquo;ve mentioned before, I&rsquo;m using Terraform for a lot of the configuration for
Vault, because that is preferable to keeping a list of commands in my
docs.</p>
<p>But the issue was: I also needed to keep the configuration for the old Vault
instance, because I needed to keep that one running during the migration as well.</p>
<p>So I started out with just adding the second Vault as another provider to my
Terraform config, via <a href="https://developer.hashicorp.com/terraform/language/providers/configuration#alias-multiple-provider-configurations">provider aliases</a>.
It looked like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;vault&#34;</span> {
</span></span><span style="display:flex;"><span>  address <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://vault.example.com:8200&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;vault&#34;</span> {
</span></span><span style="display:flex;"><span>  alias <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;k8s&#34;</span>
</span></span><span style="display:flex;"><span>  address <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://newvault.example.com:8200&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This allows me to keep configurations for two Vault instances in the same Terraform
state. I initially only created the <code>userpass</code> auth method for the new Vault,
to verify that the Terraform setup worked:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;vault_auth_backend&#34; &#34;userpass&#34;</span> {
</span></span><span style="display:flex;"><span>  type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;userpass&#34;</span>
</span></span><span style="display:flex;"><span>  path <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;userpass&#34;</span>
</span></span><span style="display:flex;"><span>  local <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;vault_auth_backend&#34; &#34;userpass-k8s&#34;</span> {
</span></span><span style="display:flex;"><span>  provider <span style="color:#f92672">=</span> <span style="color:#66d9ef">vault</span>.<span style="color:#66d9ef">k8s</span>
</span></span><span style="display:flex;"><span>  type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;userpass&#34;</span>
</span></span><span style="display:flex;"><span>  path <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;userpass&#34;</span>
</span></span><span style="display:flex;"><span>  local <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>With the <code>provider</code> setting, I could choose which Vault provider config I wanted
to use.</p>
<p>But trying a <code>terraform apply</code> with this configuration resulted in an error:</p>
<pre tabindex="0"><code>│ Error: failed to lookup token, err=Error making API request.
│
│ URL: GET https://vault.example.com:8200/v1/auth/token/lookup-self
│ Code: 403. Errors:
│
│ * 2 errors occurred:
│       * permission denied
│       * invalid token
</code></pre><p>This confused me - until I remembered that I had configured the Vault root token
for the new k8s Vault in the terminal I was running the command. Running my
customary <code>vault login -method=userpass username=myuser</code> on another shell and
executing the <code>terraform apply</code> of course also didn&rsquo;t work, because now it had
only the Vault token needed for the old Vault instance.</p>
<p>A quick look into the <a href="https://registry.terraform.io/providers/hashicorp/vault/latest/docs#vault-authentication-configuration-options">Vault Terraform provider documentation</a>
lead to the solution. I could configure one provider <a href="https://registry.terraform.io/providers/hashicorp/vault/latest/docs#token-file">with a filepath</a>
to a token file. That would be the provider for the old Vault instance. Then
I would leave the provider for the new Vault unconfigured, which would mean that
it would continue to use the <code>VAULT_TOKEN</code> environment variable. The resulting
provider config looked like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;vault&#34;</span> {
</span></span><span style="display:flex;"><span>  address <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://vault.example.com:8200&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">auth_login_token_file</span> {
</span></span><span style="display:flex;"><span>    filename <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/home/myuser/.vault-token&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;vault&#34;</span> {
</span></span><span style="display:flex;"><span>  alias <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;k8s&#34;</span>
</span></span><span style="display:flex;"><span>  address <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;https://newvault.example.com:8200&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>I would then first run the login for the old provider:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>vault login -method<span style="color:#f92672">=</span>userpass username<span style="color:#f92672">=</span>myuser
</span></span></code></pre></div><p>Then, in the same terminal, I would set the <code>VAULT_TOKEN</code> variable to the root
token of the new Vault:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>export VAULT_TOKEN<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;hvs.foobar&#34;</span>
</span></span></code></pre></div><p>And with that, I was able to run <code>terraform apply</code> without issue, and both
Vault instances were configurable.</p>
<p>Next, I needed to be able to stop using the root token for the new instance and
instead create a <code>userpass</code> login for that one as well. This, I needed to do on
the command line, because the <a href="https://registry.terraform.io/providers/hashicorp/vault/latest/docs/resources/generic_endpoint">Terraform resource</a>
that needs to be used to create a <code>userpass</code> user requires the password as part
of the Terraform resources, and I really did not want that. So I created it
on the command line:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>vault write auth/userpass/users/myuser password<span style="color:#f92672">=</span>- token_policies<span style="color:#f92672">=</span>admin token_ttl<span style="color:#f92672">=</span>4h token_max_ttl<span style="color:#f92672">=</span>4h token_bound_cidrs<span style="color:#f92672">=</span>300.300.300.12 token_type<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;default&#34;</span>
</span></span></code></pre></div><p>This will create the user <code>myuser</code> in the <code>userpass</code> backend and will ask for
the password on the command line. Tokens issued by this auth method for the user
will be valid for four hours and will only be valid when used from the <code>300.300.300.12</code>
source IP, which is my Command &amp; Control host.</p>
<p>Now, instead of exporting the root token in the <code>VAULT_TOKEN</code> variable, I could
issue this command to instead get a token for the <code>myuser</code> role:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>export VAULT_TOKEN<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>vault login -method<span style="color:#f92672">=</span>userpass -token-only username<span style="color:#f92672">=</span>myuser<span style="color:#66d9ef">)</span>
</span></span></code></pre></div><h2 id="migrating-kv-secrets">Migrating K/V secrets</h2>
<p>After I had the <code>userpass</code> login configured, I could just copy+paste all of the
Terraform resources for my Vault setup, add the <code>provider = vault.k8s</code> option,
and one <code>terraform apply</code> later, most configuration was migrated to the new
Vault instance on Kubernetes.</p>
<p>The only problem were the K/V secrets. Those are not in Terraform, because that
would have required me to put my secrets into the Terraform config files and the
Terraform state. After searching around a little, it looked like there was no
official way to run a migration of K/V secrets, so I came up with my own.</p>
<p>First, I would export the <code>data</code> field, which contains the actual secrets, as
opposed to some metadata, from the old Vault:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>vault kv get -field data -format json secret/topsecret/database-creds &gt; out.json
</span></span></code></pre></div><p>That would give me a JSON file like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;username&#34;</span>: <span style="color:#e6db74">&#34;foo&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;password&#34;</span>: <span style="color:#e6db74">&#34;bar&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That could then be imported into the new Vault like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>vault kv put secret/topsecret/database-creds @out.json
</span></span></code></pre></div><p>I did that exactly 59 times, and all of my secrets were successfully migrated
over.</p>
<h2 id="update-playbook-changes">Update playbook changes</h2>
<p>Another interesting piece of code I would like to talk about is my Homelab host
update Ansible playbook. This playbook runs updates of the host OS, Ubuntu
server in my case, including automatic reboots and k8s node drains. But I would
need to manually unseal the Vault Pods once their host was updated and rebooted.
For that, I&rsquo;m just having an Ansible task outputting the command I can copy+paste
into another terminal to do the unseal.</p>
<p>This was pretty simple up to now, with the baremetal Vault, because I could
directly contact the host being updated, because the Vault instance on there
would be the one which needs the unseal.
But, with Vault in k8s, there&rsquo;s no obvious way to determine which of the three
Vault Pods ran on the host currently being updated. I needed an approach to
find the right container.</p>
<p>The first step is to wait for the local Vault Pod on the rebooted machine to
come up again, so that it would even accept the unseal command. I did that
with the following task:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">wait for vault to be running</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tags</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">kubernetes</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">delegate_to</span>: <span style="color:#ae81ff">candchost</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become_user</span>: <span style="color:#ae81ff">candcuser</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">kubernetes.core.k8s_info</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Pod</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">vault</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">label_selectors</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">app.kubernetes.io/name=vault</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">app.kubernetes.io/instance=vault</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">field_selectors</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;spec.nodeName={{ ansible_hostname }}&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">wait</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">wait_condition</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">status</span>: <span style="color:#e6db74">&#34;True&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">type</span>: <span style="color:#e6db74">&#34;Ready&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">wait_sleep</span>: <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">wait_timeout</span>: <span style="color:#ae81ff">300</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">register</span>: <span style="color:#ae81ff">vault_pod_list</span>
</span></span></code></pre></div><p>This task uses the <a href="https://docs.ansible.com/ansible/latest/collections/kubernetes/core/k8s_info_module.html#ansible-collections-kubernetes-core-k8s-info-module">Kubernetes Ansible collection</a>
to have Ansible wait for the Vault Pod to be in <code>Ready</code> state. I&rsquo;m also saving the list
of discovered Vault Pods in a variable for later use. This task would only wait
for the Vault Pod on the host currently being updated, via the field selector.</p>
<p>Short aside: This also taught me that I could do the following:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>kubectl get pods -A --field-selector <span style="color:#e6db74">&#34;spec.nodeName=mynode&#34;</span>
</span></span></code></pre></div><p>Instead of <code>kubectl get pods -A -o wide | grep mynode</code>. After over a year of
running Kubernetes in my Homelab. &#x1f926;</p>
<p>But let&rsquo;s move on. I now had the name of the Vault Pod on the rebooted host
in the <code>vault_pod_list</code> variable, which allowed me to output a command line
I could copy+paste to unseal the Vault instance:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">unseal vault prompt</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tags</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">vault</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">pause</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">echo</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">prompt</span>: <span style="color:#e6db74">&#34;Please unseal vault: kubectl exec -it -n vault {{ vault_pod_list.resources[0].metadata.name }} -- vault operator unseal&#34;</span>
</span></span></code></pre></div><p>This is a pretty convenient way to integrate manual operations into an Ansible
play and works quite well. I see this prompt, copy the line and unseal the Pod,
and then I just hit <code>&lt;Return&gt;</code> in the shell where Ansible is running and the
play will continue.</p>
<h2 id="switching-the-certs-over-to-the-new-vaults-ca">Switching the certs over to the new Vault&rsquo;s CA</h2>
<p>If you remember from further up (and I won&rsquo;t be mad if you don&rsquo;t, looking at the
length of this post&hellip;), I was using the baremetal Vault instance to generate the
certificates for the new Vault instance. But this also meant that those certs
were relying on the old Vault&rsquo;s CA.</p>
<p>The first step was to update the CA cert in the k8s Secret used for the
VaultDynamicSecret for the Vault certificate, which I did by changing the
line in my <code>values.yaml</code> file fetching the CA:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>{{- <span style="color:#ae81ff">exec &#34;curl&#34; (list &#34;-k&#34; &#34;https://newvault:8200/v1/my-ca/ca/pem&#34;) | nindent 2 }}</span>
</span></span></code></pre></div><p>This did not have any direct effect on anything. The certificate Secret has a
TTL of 4000 hours, so won&rsquo;t try to recreate the certs anytime soon. At the same
time, Vault won&rsquo;t automatically reload a new CA either, so everything was fine.</p>
<p>Then I went into the VaultDynamicSecret and updated the Vault URL from the old
to the new Vault. This regenerated the Vault certificates. But again, Vault
itself doesn&rsquo;t react to that, so Vault was still up and running without issue.</p>
<p>Then I send <code>SIGHUP</code> to each Vault instance in turn, which triggered a configuration
reload, including fresh certificates.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>kubectl exec -it -n vault vault-0 -- sh
</span></span><span style="display:flex;"><span>kill -SIGHUP <span style="color:#66d9ef">$(</span>pidof vault<span style="color:#66d9ef">)</span>
</span></span></code></pre></div><p>And that&rsquo;s it. I added annotations to a couple of ExternalSecrets to trigger
refreshes to make sure it all worked, and it did, external-secrets successfully
got the secrets from the new Vault k8s instance.</p>
<p>This was quite a lot more work than I thought, but it was also the second-to-last
part of the migration.
Now, the only thing still missing is to migrate the control plane off of the
VMs on my extension host and onto the three Raspberry Pi 4 which previously served as
controller nodes and are now empty, thanks to the baremetal Vault having been shut down.</p>
<p>But it&rsquo;s Monday evening now, and the controller migration is more a weekend task,
because it also includes moving the MONs of the Rook Ceph cluster, and that
will need some full cluster restarts.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
