<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Konstantin </title>
    <description>The latest articles on DEV Community by Konstantin  (@kseitter).</description>
    <link>https://dev.to/kseitter</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4047963%2F05c7f286-0539-455b-9c9e-50ba42aba799.jpeg</url>
      <title>DEV Community: Konstantin </title>
      <link>https://dev.to/kseitter</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kseitter"/>
    <language>en</language>
    <item>
      <title>[Boost]</title>
      <dc:creator>Konstantin </dc:creator>
      <pubDate>Mon, 10 Aug 2026 18:31:10 +0000</pubDate>
      <link>https://dev.to/kseitter/-44b8</link>
      <guid>https://dev.to/kseitter/-44b8</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/kseitter/why-ai-agents-need-a-vault-building-openbao-for-claude-code-a-home-lab-walkthrough-idn" class="crayons-story__hidden-navigation-link"&gt;Why AI Agents Need a Vault: Building OpenBao for Claude Code (A Home Lab Walkthrough)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/kseitter" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4047963%2F05c7f286-0539-455b-9c9e-50ba42aba799.jpeg" alt="kseitter profile" class="crayons-avatar__image" width="473" height="473"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/kseitter" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Konstantin 
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Konstantin 
                
              
              &lt;div id="story-author-preview-content-4242909" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/kseitter" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4047963%2F05c7f286-0539-455b-9c9e-50ba42aba799.jpeg" class="crayons-avatar__image" alt="" width="473" height="473"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Konstantin &lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/kseitter/why-ai-agents-need-a-vault-building-openbao-for-claude-code-a-home-lab-walkthrough-idn" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 27&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/kseitter/why-ai-agents-need-a-vault-building-openbao-for-claude-code-a-home-lab-walkthrough-idn" id="article-link-4242909"&gt;
          Why AI Agents Need a Vault: Building OpenBao for Claude Code (A Home Lab Walkthrough)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/openbao"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;openbao&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/aiops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;aiops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/security"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;security&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/identity"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;identity&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/kseitter/why-ai-agents-need-a-vault-building-openbao-for-claude-code-a-home-lab-walkthrough-idn#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            26 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Why AI Agents Need a Vault: Building OpenBao for Claude Code (A Home Lab Walkthrough)</title>
      <dc:creator>Konstantin </dc:creator>
      <pubDate>Mon, 27 Jul 2026 10:18:51 +0000</pubDate>
      <link>https://dev.to/kseitter/why-ai-agents-need-a-vault-building-openbao-for-claude-code-a-home-lab-walkthrough-idn</link>
      <guid>https://dev.to/kseitter/why-ai-agents-need-a-vault-building-openbao-for-claude-code-a-home-lab-walkthrough-idn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; AI agents like Claude Code should not hold static credentials. This guide builds a single OpenBao server (Shamir-sealed) that issues short-lived, scoped secrets on demand: a signed SSH cert for a target VM, and a KV-stored API token for Home Assistant. Both the Windows and Ubuntu client authenticate with a TLS client cert, then pull exactly what the current task needs. Two wrapper scripts (&lt;code&gt;connect-target&lt;/code&gt;, &lt;code&gt;get-ha-token&lt;/code&gt;) are what Claude Code actually calls. Nothing long-lived sits on disk.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI coding agents like Claude Code are agentic: they read the environment, plan an approach, and run commands on your behalf. That is precisely why they must not hold the credentials themselves. A static SSH key or a Home Assistant token sitting in a config file is a standing liability the moment something autonomous can read and reuse it. If the agent's session is compromised, a standing credential is something to steal. A short-lived one is not. I work professionally as a Product &amp;amp; Service Manager and IT Security Architect for privileged access and secrets management, CyberArk/Idira and OpenBao, at 400,000+ employee scale. What follows is the same pattern at home-lab scale.&lt;/p&gt;

&lt;p&gt;This walkthrough builds the alternative: a single OpenBao server that hands an agent short-lived, narrowly scoped credentials on demand, for two kinds of access, SSH into a target VM, and API access to Home Assistant. Nothing outlives the task it was issued for. The agent authenticates once with a client cert, and the policy attached to that cert defines what it can pull: SSH signing for one host, read access to one KV path, nothing more. Splitting capabilities across separate certs, instead of one cert with everything, is the next step once you need to reason about blast radius per capability rather than per agent. Compromise a client cert under this model, and the attacker (or a misbehaving agent) gets a short-lived credential scoped to exactly one thing, not a standing key to everything.&lt;/p&gt;

&lt;p&gt;The server stays deliberately simple at the start: Shamir-sealed, unsealed manually after a restart. That is the appropriate level of complexity for home-lab risk. Auto-unseal is a real upgrade for an agent that must survive unattended reboots, but it is an optional extension, covered later, not a default.&lt;/p&gt;

&lt;p&gt;The pattern is not home-lab-only theory. The SSH CA, KV-backed static credentials, and PKI-issued TLS client certs here are the same mechanisms used to hand credentials to production AI agent deployments at larger scale, just smaller. A home lab is a good place to learn it because the blast radius of a mistake is a VM and a smart light, not a company.&lt;/p&gt;

&lt;p&gt;We use OpenBao, a fork of HashiCorp Vault. Mitchell Hashimoto and Armon Dadgar built Vault at HashiCorp starting in 2015, establishing the principle that credentials should be short-lived and centrally issued rather than static and copy-pasted forever. When HashiCorp moved Vault to a Business Source License in 2023, the community forked the last open-source release and continued development as OpenBao, now under the Linux Foundation.&lt;/p&gt;

&lt;p&gt;The home lab is the setting because it is a safe, low-stakes place to learn a pattern that scales directly to production agent deployments: authenticate the agent's identity once with a TLS client cert, then let it pull exactly the credential a given task needs, nothing else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture at a glance
&lt;/h2&gt;

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

&lt;p&gt;&lt;strong&gt;Why this shape:&lt;/strong&gt; one OpenBao server handles everything. It is Shamir-sealed, unsealed manually after a restart with key shares split across custodians, and hosts two capabilities: an SSH CA for reaching the target, and a KV store for static credentials like the Home Assistant token. Both sit behind the same client-cert identity. One login, then one of two kinds of short-lived secret depending on the task.&lt;/p&gt;

&lt;p&gt;The build has &lt;strong&gt;two clients&lt;/strong&gt; authenticating the same way, TLS client cert, then a short-lived token, then whichever secret the task needs: a Windows box running Claude Code, and an Ubuntu box doing the identical sequence from a shell script.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installing OpenBao
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Create the service user, data directory, and TLS material location first&lt;/strong&gt;, so the download and verification steps below can run as that user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;useradd &lt;span class="nt"&gt;--system&lt;/span&gt; &lt;span class="nt"&gt;--home&lt;/span&gt; /etc/openbao &lt;span class="nt"&gt;--shell&lt;/span&gt; /bin/false openbao
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /opt/openbao/data /opt/openbao/downloads /etc/openbao/tls
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; openbao:openbao /opt/openbao /etc/openbao
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Download and verify the release&lt;/strong&gt;, before installing anything. There is no first-party apt repository; packages are distributed as direct downloads and via EPEL for RHEL-family distros. This is the verified method for Ubuntu/Debian:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl jq gnupg dpkg

&lt;span class="c"&gt;# Pinned for repeatable installs across nodes, check&lt;/span&gt;
&lt;span class="c"&gt;# https://github.com/openbao/openbao/releases for newer versions when you revisit this&lt;/span&gt;
&lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"2.6.1"&lt;/span&gt;
&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;   &lt;span class="c"&gt;# amd64 or arm64&lt;/span&gt;

&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; openbao &lt;span class="nt"&gt;-H&lt;/span&gt; bash &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
set -euo pipefail
cd /opt/openbao/downloads
VERSION="&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"
ARCH="&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"
curl -LO "https://github.com/openbao/openbao/releases/download/v&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="sh"&gt;{VERSION}/openbao_&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="sh"&gt;{VERSION}_linux_&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="sh"&gt;{ARCH}.deb"
curl -LO "https://github.com/openbao/openbao/releases/download/v&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="sh"&gt;{VERSION}/checksums.txt"
curl -LO "https://github.com/openbao/openbao/releases/download/v&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="sh"&gt;{VERSION}/checksums.txt.gpgsig"
curl -LO https://openbao.org/assets/openbao-gpg-pub-20240618.asc
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verification is required here, not optional, before the package is installed.&lt;/strong&gt; Clean source is the entire point of pinning a version at all; skipping this step makes the pinned version number meaningless. OpenBao publishes &lt;code&gt;checksums.txt&lt;/code&gt;, a GPG signature (&lt;code&gt;checksums.txt.gpgsig&lt;/code&gt;), and a Sigstore/cosign bundle with each release.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; openbao &lt;span class="nt"&gt;-H&lt;/span&gt; bash &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
set -euo pipefail
cd /opt/openbao/downloads

# Verify the .deb matches the published checksum
sha256sum --ignore-missing --check checksums.txt

# Verify the checksums file itself was signed by the OpenBao project,
# confirming the checksum file was not tampered with
gpg2 --import openbao-gpg-pub-20240618.asc
gpg2 --verify checksums.txt.gpgsig checksums.txt
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect &lt;code&gt;gpg: Good signature from "OpenBao &amp;lt;openbao@lists.lfedge.org&amp;gt;" [unknown]&lt;/code&gt;. GPG will also warn &lt;code&gt;WARNING: This key is not certified with a trusted signature! There is no indication that the signature belongs to the owner&lt;/code&gt;. That is expected unless you have separately verified and locally signed the key; it does not mean the check failed. If either the checksum or the signature check fails, stop here, do not proceed to install a &lt;code&gt;.deb&lt;/code&gt; that failed verification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Only now, install the verified package.&lt;/strong&gt; This is the one step that genuinely requires root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"/opt/openbao/downloads/openbao_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_linux_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.deb"&lt;/span&gt;
bao &lt;span class="nt"&gt;-h&lt;/span&gt;   &lt;span class="c"&gt;# confirms installation and PATH availability&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On RHEL/Fedora, the equivalent is EPEL, which handles verification through the distribution's own package signing instead of the manual steps above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; epel-release
&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; openbao
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Place your TLS cert/key in &lt;code&gt;/etc/openbao/tls/&lt;/code&gt;. A self-signed cert is sufficient for a home lab. If you do not already have an internal CA, generate one covering the server's hostname/IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate a CA key + self-signed CA cert (reuse this same CA for every client cert later too)&lt;/span&gt;
openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; ca-key.pem 4096
openssl req &lt;span class="nt"&gt;-x509&lt;/span&gt; &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-nodes&lt;/span&gt; &lt;span class="nt"&gt;-key&lt;/span&gt; ca-key.pem &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="nt"&gt;-days&lt;/span&gt; 3650 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/CN=homelab-internal-ca"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; ca.pem

&lt;span class="c"&gt;# Generate the server's key + a CSR&lt;/span&gt;
openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; server-key.pem 4096
openssl req &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-key&lt;/span&gt; server-key.pem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/CN=openbao.homelab.local"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; server.csr

&lt;span class="c"&gt;# Sign the server cert with your CA, including SANs for hostname + IP&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; server-ext.cnf &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
subjectAltName = DNS:openbao.homelab.local,IP:&amp;lt;server-ip&amp;gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;openssl x509 &lt;span class="nt"&gt;-req&lt;/span&gt; &lt;span class="nt"&gt;-in&lt;/span&gt; server.csr &lt;span class="nt"&gt;-CA&lt;/span&gt; ca.pem &lt;span class="nt"&gt;-CAkey&lt;/span&gt; ca-key.pem &lt;span class="nt"&gt;-CAcreateserial&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-out&lt;/span&gt; cert.pem &lt;span class="nt"&gt;-days&lt;/span&gt; 825 &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="nt"&gt;-extfile&lt;/span&gt; server-ext.cnf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the results into place and lock down the private key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo cp &lt;/span&gt;cert.pem server-key.pem ca.pem /etc/openbao/tls/
&lt;span class="nb"&gt;sudo mv&lt;/span&gt; /etc/openbao/tls/server-key.pem /etc/openbao/tls/key.pem
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; openbao:openbao /etc/openbao/tls
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;640 /etc/openbao/tls/key.pem
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;644 /etc/openbao/tls/cert.pem /etc/openbao/tls/ca.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matches the paths referenced in the &lt;code&gt;bao.hcl&lt;/code&gt; listener block below (&lt;code&gt;tls_cert_file&lt;/code&gt;/&lt;code&gt;tls_key_file&lt;/code&gt;) and the &lt;code&gt;BAO_CACERT&lt;/code&gt; used later. Keep &lt;code&gt;ca-key.pem&lt;/code&gt; off the server, in secure storage. It signs the Windows and Ubuntu client certs later; if it leaks, every certificate it ever signed is suspect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Export &lt;code&gt;ca.pem&lt;/code&gt; to every other machine that needs to talk to this server.&lt;/strong&gt; &lt;code&gt;ca.pem&lt;/code&gt; is the CA's public certificate, the public half only, it contains no secret material and is safe to copy anywhere. It is what lets a client verify "was this server's TLS cert actually signed by my CA," the same role a public key plays in any signing scheme. &lt;code&gt;ca-key.pem&lt;/code&gt;, by contrast, is the private half, that one never leaves secure storage.&lt;/p&gt;

&lt;p&gt;To confirm what's actually inside it before trusting it anywhere, &lt;code&gt;ca.pem&lt;/code&gt; is a certificate, not a bare key, so it's human-readable with &lt;code&gt;openssl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl x509 &lt;span class="nt"&gt;-in&lt;/span&gt; /etc/openbao/tls/ca.pem &lt;span class="nt"&gt;-text&lt;/span&gt; &lt;span class="nt"&gt;-noout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for &lt;code&gt;Subject: CN = homelab-internal-ca&lt;/code&gt; and &lt;code&gt;X509v3 Basic Constraints: CA:TRUE&lt;/code&gt;, that confirms it's the CA certificate, not a leaf/server certificate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Write the config file&lt;/strong&gt; (&lt;code&gt;/etc/openbao/bao.hcl&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Client API address&lt;/span&gt;
&lt;span class="nx"&gt;api_addr&lt;/span&gt;     &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://&amp;lt;server-ip&amp;gt;:8200"&lt;/span&gt; 

&lt;span class="c1"&gt;# Node-to-node Raft communication address, only relevant once you add more nodes&lt;/span&gt;

&lt;span class="nx"&gt;cluster_addr&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://&amp;lt;server-ip&amp;gt;:8201"&lt;/span&gt; 

&lt;span class="nx"&gt;storage&lt;/span&gt; &lt;span class="s2"&gt;"raft"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/opt/openbao/data"&lt;/span&gt;
  &lt;span class="nx"&gt;node_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"openbao-primary"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;listener&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;address&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.0.0.0:8200"&lt;/span&gt;
  &lt;span class="nx"&gt;cluster_address&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.0.0.0:8201"&lt;/span&gt;
  &lt;span class="nx"&gt;tls_cert_file&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/etc/openbao/tls/cert.pem"&lt;/span&gt;
  &lt;span class="nx"&gt;tls_key_file&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/etc/openbao/tls/key.pem"&lt;/span&gt;
  &lt;span class="nx"&gt;tls_disable&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="c1"&gt;# (Set to 0 if you are using TLS certificates)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Enable and start via systemd.&lt;/strong&gt; The package installs a unit file; if you used the raw binary, create one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/openbao.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;OpenBao&lt;/span&gt;
&lt;span class="py"&gt;Requires&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;openbao&lt;/span&gt;
&lt;span class="py"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;openbao&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/bao server -config=/etc/openbao/bao.hcl&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="py"&gt;LimitNOFILE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;65536&lt;/span&gt;
&lt;span class="py"&gt;Capabilities&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;CAP_IPC_LOCK+ep&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; openbao
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status openbao
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Download the &lt;code&gt;bao&lt;/code&gt; CLI to a client of your choice.&lt;/strong&gt; This can be your admin workstation or laptop; it does not need to be the server. Same GitHub-releases approach as the server install, the CLI-only binary for your OS/architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Linux example, adjust ARCH/OS for macOS or Windows as needed&lt;/span&gt;
&lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"2.6.1"&lt;/span&gt;   &lt;span class="c"&gt;# keep this matched to the server version above&lt;/span&gt;
&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;dpkg &lt;span class="nt"&gt;--print-architecture&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;   &lt;span class="c"&gt;# amd64 or arm64&lt;/span&gt;

curl &lt;span class="nt"&gt;-LO&lt;/span&gt; &lt;span class="s2"&gt;"https://github.com/openbao/openbao/releases/download/v&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/openbao_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_linux_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.deb"&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"openbao_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_linux_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.deb"&lt;/span&gt;

bao &lt;span class="nt"&gt;-h&lt;/span&gt;   &lt;span class="c"&gt;# confirms installation and PATH availability&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Windows, download the &lt;code&gt;.zip&lt;/code&gt; release from the same GitHub releases page, extract &lt;code&gt;bao.exe&lt;/code&gt;, and add it to &lt;code&gt;PATH&lt;/code&gt;. On macOS, &lt;code&gt;brew install openbao&lt;/code&gt; works with Homebrew.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Set the CLI's target and confirm it is reachable:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;BAO_ADDR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;server-ip&amp;gt;:8200"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;BAO_CACERT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/home/&amp;lt;user&amp;gt;/ca.pem"&lt;/span&gt;
bao status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;Sealed: true&lt;/code&gt; immediately after first start. That is expected; init and unseal follow next.&lt;/p&gt;




&lt;h2&gt;
  
  
  Initialize and unseal
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bao operator init &lt;span class="nt"&gt;-key-shares&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5 &lt;span class="nt"&gt;-key-threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns 5 unseal key shares and a root token. Both require real custody, not a text file on the desktop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storing the Shamir key shares:&lt;/strong&gt; put each share into a password manager entry. KeePassXC is a solid home-lab choice, ideally with the database unlocked via a hardware key (YubiKey, via challenge-response or as a second factor) rather than a master password alone. Distribute the five entries across separate databases, custodians, or locations, a 3-of-5 threshold means no single person or compromised vault can unseal alone. Do not store all five shares in one place; that defeats the purpose of splitting them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The root token is the single most important secret this command produces.&lt;/strong&gt; It bypasses every policy and can read and write anything. It is also what bootstraps everything else in this playbook: enabling secrets engines, writing policies, registering cert auth. Until those scoped policies exist, the root token is the only credential capable of that setup work. Treat the following sections as "root token required" and store it with at least as much care as the key shares. Once the policies in this guide exist, stop using the root token for day-to-day operations and consider revoking it (see "Going further" below).&lt;/p&gt;

&lt;p&gt;Unseal, repeating with three different key shares:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bao operator unseal
bao operator unseal
bao operator unseal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;&lt;strong&gt;Operational note:&lt;/strong&gt; every restart, a patch, a Proxmox host reboot, a power event, brings the server back up sealed. Someone must run &lt;code&gt;bao operator unseal&lt;/code&gt; three times with different shares before anything downstream, the agent's SSH or KV access, works again. For a home lab this is a reasonable trade: a simple, single-node setup, at the cost of a few minutes of manual attention after a restart. A short runbook and an alert on &lt;code&gt;sealed=true&lt;/code&gt; are worth setting up.&lt;/p&gt;




&lt;h2&gt;
  
  
  Enable the secrets engines: SSH, PKI, and KV
&lt;/h2&gt;

&lt;p&gt;Now we're going to enable the secret engines. You can find all available on &lt;a href="https://openbao.org/docs/secrets/" rel="noopener noreferrer"&gt;https://openbao.org/docs/secrets/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before running anything below, authenticate the CLI with the root token&lt;/strong&gt;, and keep it out of shell history while you're at it. The root token is sensitive enough that it shouldn't sit in &lt;code&gt;~/.bash_history&lt;/code&gt; in plain text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Prevent lines containing the token (or any bao/BAO command) from being written to history&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'export HISTIGNORE="&amp;amp;:*[Bb][Aa][Oo]*"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.bashrc
&lt;span class="nb"&gt;source&lt;/span&gt; ~/.bashrc

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;BAO_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;root-token&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every &lt;code&gt;bao&lt;/code&gt; command in this section and the next two run with this token in scope. It stays required until the scoped policies further down exist, at which point "Retiring the root token" (in "Going further") covers moving off it.&lt;/p&gt;

&lt;h3&gt;
  
  
  SSH secrets engine (CA mode) for the target host
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bao secrets &lt;span class="nb"&gt;enable &lt;/span&gt;ssh
bao write ssh/config/ca &lt;span class="nv"&gt;generate_signing_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the returned public key; it gets installed on the target machine shortly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bao write ssh/roles/target-access -&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
{
  "allow_user_certificates": true,
  "allowed_users": "opuser",
  "default_extensions": [{"permit-pty": ""}],
  "key_type": "ca",
  "default_user": "opuser",
  "ttl": "15m",
  "max_ttl": "1h"
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  PKI secrets engine for issuing client certs
&lt;/h3&gt;

&lt;p&gt;This engine is what issues the Windows and Ubuntu client certificates later, on demand, through OpenBao's API instead of by hand with &lt;code&gt;openssl&lt;/code&gt; each time. It is unrelated to the &lt;code&gt;cert&lt;/code&gt; auth method itself, which only needs a CA certificate to verify against; the PKI engine is a convenience for producing certs signed by that same CA.&lt;br&gt;
Reuse the internal CA created earlier for the server's own TLS listener, rather than standing up a second one. Run this from wherever &lt;code&gt;ca-key.pem&lt;/code&gt; actually lives, the secure storage location from the earlier step, not the server itself, &lt;code&gt;ca-key.pem&lt;/code&gt; was deliberately never copied there. The &lt;code&gt;bao&lt;/code&gt; CLI talks to the server over the network, so it does not need to run locally on the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bao secrets &lt;span class="nb"&gt;enable &lt;/span&gt;pki
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before importing, confirm the key actually matches the cert, a mismatched pair is the most common cause of the import silently failing to attach a usable key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl x509 &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-modulus&lt;/span&gt; &lt;span class="nt"&gt;-in&lt;/span&gt; ca.pem | openssl md5
openssl rsa  &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-modulus&lt;/span&gt; &lt;span class="nt"&gt;-in&lt;/span&gt; ca-key.pem | openssl md5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both hashes must match. If they don't, this is not the CA's actual private key, track down the real &lt;code&gt;ca-key.pem&lt;/code&gt; from the original generation step before continuing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bao write pki/config/ca &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;pem_bundle&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /home/&amp;lt;user&amp;gt;/ca-key.pem /home/&amp;lt;user&amp;gt;/ca.pem&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Importing a CA creates an issuer, but does not automatically make it the mount's default.&lt;/strong&gt; Skipping this step is the most common cause of &lt;code&gt;no default issuer currently configured&lt;/code&gt; errors on the first &lt;code&gt;pki/issue/...&lt;/code&gt; call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;ISSUER_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;bao list &lt;span class="nt"&gt;-format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json pki/issuers | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[0]'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
bao write pki/config/issuers &lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ISSUER_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify it took, and that the issuer actually has a key attached, &lt;code&gt;pki/issue&lt;/code&gt; will fail with "unable to fetch corresponding key for issuer" if not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bao &lt;span class="nb"&gt;read &lt;/span&gt;pki/config/issuers
bao &lt;span class="nb"&gt;read &lt;/span&gt;pki/issuer/&lt;span class="nv"&gt;$ISSUER_ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  KV v2 secrets engine for static credentials (Home Assistant example)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Create the Home Assistant long-lived access token.&lt;/strong&gt; This is Home Assistant's equivalent of an API key. It authenticates REST API calls the same way a password would, but is scoped to API access and revocable independently of your login:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log into Home Assistant and click your profile icon/name in the bottom left.&lt;/li&gt;
&lt;li&gt;Scroll down to the &lt;strong&gt;Security&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Long-Lived Access Tokens&lt;/strong&gt;, click &lt;strong&gt;Create Token&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Give it a descriptive name, for example &lt;code&gt;openbao-claude-code&lt;/code&gt;, to distinguish it from other tokens and revoke it individually if needed.&lt;/li&gt;
&lt;li&gt;Home Assistant shows the token exactly once. Copy it immediately; it cannot be viewed again after closing the dialog, only revoked and reissued.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Home Assistant's long-lived tokens do not expire on their own. Treat this like any other static credential: it does not belong in a config file, which is the reason to store it in OpenBao's KV engine rather than &lt;code&gt;secrets.yaml&lt;/code&gt; on the client.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Store it in OpenBao:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bao secrets &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;homeassistant kv-v2

bao kv put homeassistant/api &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;the-token-you-just-copied&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://homeassistant.local:8123"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Verify it landed correctly:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bao kv get homeassistant/api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;This is the general pattern for any static credential an agent needs, an API key, a webhook secret, a database password: a KV v2 mount, one path per credential, read-only policies scoped per path. Home Assistant is this document's concrete example.&lt;/p&gt;




&lt;h2&gt;
  
  
  Policies
&lt;/h2&gt;

&lt;p&gt;All capabilities get their own policy, attached to each client identity from the start, so an authenticated agent can reach either one depending on the task.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. KV read and SSH signing policy
&lt;/h3&gt;

&lt;p&gt;Now we are creating a KV read and SSH signing policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ssh-target-sign-policy.hcl &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
path "ssh/sign/target-access" {
  capabilities = ["create", "update"]
}

path "ssh/roles/target-access" {
  capabilities = ["read"]
}
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; homeassistant-read-policy.hcl &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
path "homeassistant/data/api" {
  capabilities = ["read"]
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bao policy write ssh-target-sign ssh-target-sign-policy.hcl
bao policy write homeassistant-read homeassistant-read-policy.hcl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. PKI issuance policy
&lt;/h3&gt;

&lt;p&gt;Admin-side, for issuing client certs once per client during setup, not used by the clients themselves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; pki-issue-admin-policy.hcl &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
path "pki/issue/windows-client" {
  capabilities = ["create", "update"]
}

path "pki/issue/ubuntu-client" {
  capabilities = ["create", "update"]
}

path "pki/roles/windows-client" {
  capabilities = ["read"]
}

path "pki/roles/ubuntu-client" {
  capabilities = ["read"]
}
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;bao policy write pki-issue-admin pki-issue-admin-policy.hcl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both client identities get their own PKI role, issued the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bao write pki/roles/windows-client &lt;span class="nv"&gt;allow_any_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;&lt;span class="nv"&gt;max_ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;24h
bao write pki/roles/ubuntu-client  &lt;span class="nv"&gt;allow_any_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;&lt;span class="nv"&gt;max_ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;24h
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Cert-auth registration, both policies attached from the start
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;cert&lt;/code&gt; auth method maps each client cert to both policies via its own &lt;code&gt;auth/cert/certs/&amp;lt;name&amp;gt;&lt;/code&gt; entry. The agent's identity gets both capabilities in one step here, not extended later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bao auth &lt;span class="nb"&gt;enable &lt;/span&gt;cert

bao write auth/cert/certs/windows-client &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;display_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"claude-cli-windows"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;policies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ssh-target-sign,homeassistant-read"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;certificate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;@/home/&amp;lt;user&amp;gt;/ca.pem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1h

bao write auth/cert/certs/ubuntu-client &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;display_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"claude-cli-ubuntu"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;policies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ssh-target-sign,homeassistant-read"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;certificate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;@/home/&amp;lt;user&amp;gt;/ca.pem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1h
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both clients land on the identical pair of policies: same blast radius, same TTLs. To keep SSH and Home Assistant access separated, so a compromised cert cannot reach both, split them across two certs per client instead: one with only &lt;code&gt;ssh-target-sign&lt;/code&gt;, one with only &lt;code&gt;homeassistant-read&lt;/code&gt;. This is a real tradeoff between convenience and blast radius, and one the agent's threat model should drive.&lt;/p&gt;




&lt;h2&gt;
  
  
  Configuring target to trust the server's SSH CA
&lt;/h2&gt;

&lt;p&gt;This is what lets both clients log in without any pre-provisioned key: the target does not trust &lt;em&gt;keys&lt;/em&gt;, it trusts the &lt;em&gt;CA&lt;/em&gt;. Once configured, any cert OpenBao signs off that CA, Windows or Ubuntu client, is accepted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Confirm OpenSSH server is installed and running:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; openssh-server
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; ssh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Create the target login account (matches &lt;code&gt;default_user&lt;/code&gt;/&lt;code&gt;allowed_users&lt;/code&gt; in the &lt;code&gt;target-access&lt;/code&gt; role):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;useradd &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /bin/bash opuser
&lt;span class="c"&gt;# Give it sudo only if the role's use case actually needs it, don't default to this&lt;/span&gt;
&lt;span class="c"&gt;# sudo usermod -aG sudo opuser&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Fetch the CA public key from the server and install it as a trusted signer:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On the OpenBao server (or copy the value from the earlier `bao write ssh/config/ca` output)&lt;/span&gt;
bao &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-field&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;public_key ssh/config/ca &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/trusted-user-ca-keys.pem

&lt;span class="c"&gt;# On target&lt;/span&gt;
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/ssh
&lt;span class="nb"&gt;sudo mv&lt;/span&gt; /tmp/trusted-user-ca-keys.pem /etc/ssh/trusted-user-ca-keys.pem
&lt;span class="nb"&gt;sudo chown &lt;/span&gt;root:root /etc/ssh/trusted-user-ca-keys.pem
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;644 /etc/ssh/trusted-user-ca-keys.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Point sshd at it:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/ssh/sshd_config.d/openbao-ca.conf&lt;/span&gt;
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/ssh/sshd_config.d/openbao-ca.conf &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
TrustedUserCAKeys /etc/ssh/trusted-user-ca-keys.pem
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single line is sufficient for the CA trust; &lt;code&gt;allowed_users&lt;/code&gt;/&lt;code&gt;default_user&lt;/code&gt; on the OpenBao role already restrict who a signed cert can log in as. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Validate the config and restart sshd:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;sshd &lt;span class="nt"&gt;-t&lt;/span&gt;     &lt;span class="c"&gt;# syntax check before you restart and lock yourself out&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart ssh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Test from a client.&lt;/strong&gt; This step generates the test files itself, &lt;code&gt;id_ephemeral&lt;/code&gt; and &lt;code&gt;id_ephemeral-cert.pub&lt;/code&gt; do not exist yet at this point, they are created here. Run this from any machine with the &lt;code&gt;bao&lt;/code&gt; CLI configured and network access to the target's SSH port, the target itself works fine for this. &lt;/p&gt;

&lt;p&gt;A note on the tool: use &lt;code&gt;ssh-keygen&lt;/code&gt;, not raw &lt;code&gt;openssl&lt;/code&gt;, to generate the keypair. &lt;code&gt;ssh-keygen&lt;/code&gt; uses OpenSSL's crypto underneath, but it writes keys in OpenSSH's own format, which is what OpenBao's &lt;code&gt;ssh/sign&lt;/code&gt; endpoint expects as input and what &lt;code&gt;sshd&lt;/code&gt; expects on the wire. A key generated with plain &lt;code&gt;openssl genpkey&lt;/code&gt;/&lt;code&gt;openssl req&lt;/code&gt; is a different format and would need converting with &lt;code&gt;ssh-keygen -i&lt;/code&gt; before it could be signed; simplest to generate it in the right format from the start. &lt;code&gt;ssh-keygen&lt;/code&gt; is already present wherever &lt;code&gt;openssh-client&lt;/code&gt; is installed, which &lt;code&gt;openssh-server&lt;/code&gt; pulls in as a dependency, so no extra install is needed on the target.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate a throwaway keypair for this test&lt;/span&gt;
ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 &lt;span class="nt"&gt;-f&lt;/span&gt; id_ephemeral &lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt;

&lt;span class="c"&gt;# Have OpenBao sign the public half, this is what actually grants access,&lt;/span&gt;
&lt;span class="c"&gt;# the signed cert, not the keypair itself&lt;/span&gt;
bao write &lt;span class="nt"&gt;-field&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;signed_key ssh/sign/target-access &lt;span class="nv"&gt;public_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;@id_ephemeral.pub &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; id_ephemeral-cert.pub

&lt;span class="c"&gt;# Connect using the private key plus its signed certificate&lt;/span&gt;
ssh &lt;span class="nt"&gt;-i&lt;/span&gt; id_ephemeral &lt;span class="nt"&gt;-i&lt;/span&gt; id_ephemeral-cert.pub opuser@&amp;lt;ubuntu-target&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Clean up the throwaway files once the test passes, they are only for this manual check, the wrapper scripts later generate and discard their own on every connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; id_ephemeral id_ephemeral.pub id_ephemeral-cert.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful login with no password/key prompt confirms the target trusts the CA correctly. If it falls back to a password or key prompt, check &lt;code&gt;sudo journalctl -u ssh -f&lt;/code&gt; on the target while connecting. Common causes: a mismatched &lt;code&gt;allowed_users&lt;/code&gt;/&lt;code&gt;default_user&lt;/code&gt; on the OpenBao role, an expired cert TTL, or &lt;code&gt;TrustedUserCAKeys&lt;/code&gt; pointing at the wrong file.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ubuntu client, shell-native
&lt;/h2&gt;

&lt;p&gt;The Ubuntu client holds a TLS client cert, authenticates to OpenBao, gets a short-lived token, then either signs an ephemeral SSH key or reads the KV secret depending on the task. Linux has no Certificate Store, so the cert/key live on disk with tight permissions instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Issue the cert (admin side, using the &lt;code&gt;pki-issue-admin&lt;/code&gt; policy)
&lt;/h3&gt;

&lt;p&gt;Run the following from the client which already has the BAO_TOKEN in the cli&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bao write &lt;span class="nt"&gt;-format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json pki/issue/ubuntu-client &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;common_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"claude-cli-ubuntu"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"24h"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ubuntu-client-cert.json

jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.data.certificate'&lt;/span&gt; ubuntu-client-cert.json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; client.pem
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.data.private_key'&lt;/span&gt; ubuntu-client-cert.json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; client-key.pem
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.data.issuing_ca'&lt;/span&gt; ubuntu-client-cert.json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; issuing-ca.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Place the material on the Ubuntu client with tight permissions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/openbao-client
&lt;span class="nb"&gt;sudo mv &lt;/span&gt;client.pem client-key.pem issuing-ca.pem /etc/openbao-client/

&lt;span class="c"&gt;# Lock the private key down to the service account that runs Claude Code / the wrapper scripts&lt;/span&gt;
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &amp;lt;service-account&amp;gt; /etc/openbao-client/client-key.pem
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;640 /etc/openbao-client/client-key.pem
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;644 /etc/openbao-client/client.pem /etc/openbao-client/issuing-ca.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For higher assurance than a file on disk, consider the &lt;code&gt;pass&lt;/code&gt;/&lt;code&gt;gpg-agent&lt;/code&gt; route or a TPM-backed key via &lt;code&gt;openssl engine&lt;/code&gt;/&lt;code&gt;pkcs11&lt;/code&gt;. The file-based approach above is the reasonable home-lab default.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;code&gt;connect-target.sh&lt;/code&gt;, SSH access
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# Authenticates to OpenBao via TLS client cert, signs an ephemeral&lt;/span&gt;
&lt;span class="c"&gt;# SSH key for target access, and connects.&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# Usage:&lt;/span&gt;
&lt;span class="c"&gt;#   ./connect-target.sh                  # interactive shell&lt;/span&gt;
&lt;span class="c"&gt;#   ./connect-target.sh "systemctl status nginx"   # one-off command&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;BAO_ADDR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://openbao.homelab.local:8200"&lt;/span&gt;
&lt;span class="nv"&gt;CLIENT_CERT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/etc/openbao-client/client.pem"&lt;/span&gt;
&lt;span class="nv"&gt;CLIENT_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/etc/openbao-client/client-key.pem"&lt;/span&gt;
&lt;span class="nv"&gt;CA_CERT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/etc/openbao-client/issuing-ca.pem"&lt;/span&gt;
&lt;span class="nv"&gt;SSH_ROLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"target-access"&lt;/span&gt;
&lt;span class="nv"&gt;SSH_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"opuser"&lt;/span&gt;
&lt;span class="nv"&gt;TARGET_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"target.homelab.local"&lt;/span&gt;
&lt;span class="nv"&gt;COMMAND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nv"&gt;KEY_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; /tmp/bao-ssh-XXXXXX&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;'rm -rf "$KEY_DIR"'&lt;/span&gt; EXIT

&lt;span class="c"&gt;# --- 1. Authenticate to OpenBao via client cert ---&lt;/span&gt;
&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--cert&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLIENT_CERT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--key&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLIENT_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--cacert&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CA_CERT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BAO_ADDR&lt;/span&gt;&lt;span class="s2"&gt;/v1/auth/cert/login"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.auth.client_token'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"null"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"OpenBao login did not return a token."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# --- 2. Generate an ephemeral local SSH keypair ---&lt;/span&gt;
ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEY_DIR&lt;/span&gt;&lt;span class="s2"&gt;/id_ephemeral"&lt;/span&gt; &lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt;

&lt;span class="c"&gt;# --- 3. Request a signed cert from OpenBao's ssh secrets engine ---&lt;/span&gt;
&lt;span class="nv"&gt;PUB_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEY_DIR&lt;/span&gt;&lt;span class="s2"&gt;/id_ephemeral.pub"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--cacert&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CA_CERT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Vault-Token: &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;public_key&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$PUB_KEY&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BAO_ADDR&lt;/span&gt;&lt;span class="s2"&gt;/v1/ssh/sign/&lt;/span&gt;&lt;span class="nv"&gt;$SSH_ROLE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.data.signed_key'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEY_DIR&lt;/span&gt;&lt;span class="s2"&gt;/id_ephemeral-cert.pub"&lt;/span&gt;

&lt;span class="c"&gt;# --- 4. Connect ---&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COMMAND&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEY_DIR&lt;/span&gt;&lt;span class="s2"&gt;/id_ephemeral"&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEY_DIR&lt;/span&gt;&lt;span class="s2"&gt;/id_ephemeral-cert.pub"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SSH_USER&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="nv"&gt;$TARGET_HOST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COMMAND&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEY_DIR&lt;/span&gt;&lt;span class="s2"&gt;/id_ephemeral"&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KEY_DIR&lt;/span&gt;&lt;span class="s2"&gt;/id_ephemeral-cert.pub"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SSH_USER&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="nv"&gt;$TARGET_HOST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# --- 5. Cleanup happens automatically via the trap above ---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x connect-target.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verifying the connection.&lt;/strong&gt; Run this from the same directory as &lt;code&gt;connect-target.sh&lt;/code&gt;, it calls the script directly, so it needs to be run alongside it, or with a path to it if you've placed it elsewhere. Run it as a cheap, unambiguous check.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./connect-target.sh
./connect-target.sh &lt;span class="s2"&gt;"whoami"&lt;/span&gt;
./connect-target.sh &lt;span class="s2"&gt;"systemctl status sshd"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  4. &lt;code&gt;get-ha-token.sh&lt;/code&gt;, Home Assistant API access
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# Fetches the Home Assistant API token from OpenBao and prints&lt;/span&gt;
&lt;span class="c"&gt;# it as shell-sourceable export statements.&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# Usage:&lt;/span&gt;
&lt;span class="c"&gt;#   source ./get-ha-token.sh&lt;/span&gt;
&lt;span class="c"&gt;#   curl -s -H "Authorization: Bearer $HA_TOKEN" "$HA_BASE_URL/api/states"&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;BAO_ADDR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://openbao.homelab.local:8200"&lt;/span&gt;
&lt;span class="nv"&gt;CLIENT_CERT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/etc/openbao-client/client.pem"&lt;/span&gt;
&lt;span class="nv"&gt;CLIENT_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/etc/openbao-client/client-key.pem"&lt;/span&gt;
&lt;span class="nv"&gt;CA_CERT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/etc/openbao-client/issuing-ca.pem"&lt;/span&gt;

&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--cert&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLIENT_CERT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--key&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLIENT_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--cacert&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CA_CERT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BAO_ADDR&lt;/span&gt;&lt;span class="s2"&gt;/v1/auth/cert/login"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.auth.client_token'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nv"&gt;SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--cacert&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CA_CERT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Vault-Token: &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BAO_ADDR&lt;/span&gt;&lt;span class="s2"&gt;/v1/homeassistant/data/api"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;HA_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SECRET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.data.data.token'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;HA_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SECRET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.data.data.base_url'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"HA_TOKEN and HA_BASE_URL set for this shell."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make it executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x get-ha-token.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verifying the connection.&lt;/strong&gt; Home Assistant's REST API has a lightweight root endpoint (&lt;code&gt;/api/&lt;/code&gt;) that confirms the API is up and the token is valid. Of course you need to have any api available in your HA setup. I've chosen the ecotracker, swap this for any entity you actually have.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ./get-ha-token.sh
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$HA_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HA_BASE_URL&lt;/span&gt;&lt;span class="s2"&gt;/api/states/sensor.ecotracker_total_grid_export"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also do an additional check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ./get-ha-token.sh

&lt;span class="nv"&gt;CHECK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$HA_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HA_BASE_URL&lt;/span&gt;&lt;span class="s2"&gt;/api/"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CHECK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"200"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Home Assistant reachable, token valid."&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Home Assistant check failed with HTTP &lt;/span&gt;&lt;span class="nv"&gt;$CHECK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;401&lt;/code&gt; means the token from OpenBao is wrong or was revoked in Home Assistant. A connection failure, curl returning no code or &lt;code&gt;000&lt;/code&gt;, means &lt;code&gt;HA_BASE_URL&lt;/code&gt; or the network path is wrong. Distinguish the two before assuming a later, more specific API call failed for some other reason.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Wiring into Claude Code on Linux:&lt;/strong&gt; point &lt;code&gt;CLAUDE.md&lt;/code&gt; at both scripts, for example: &lt;em&gt;"To run commands on target, always use &lt;code&gt;./connect-target.sh '&amp;lt;cmd&amp;gt;'&lt;/code&gt;. To call the Home Assistant API, first run &lt;code&gt;source ./get-ha-token.sh&lt;/code&gt; to populate &lt;code&gt;HA_TOKEN&lt;/code&gt;. Never hardcode credentials or ask the user for either directly."&lt;/em&gt; This keeps the agent from reaching for a raw &lt;code&gt;ssh&lt;/code&gt; call or a hardcoded token.&lt;/p&gt;

&lt;p&gt;My Ubuntu CLAUDE.md:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Ubuntu / Bash Operations&lt;/span&gt;

When executing commands on the target machine or interacting with the Home Assistant API in this project, &lt;span class="gs"&gt;**never use raw `ssh` or hardcode API tokens**&lt;/span&gt;. Always use the provided Bash wrapper scripts to handle OpenBao authentication and routing automatically. Never ask the user for credentials.

&lt;span class="gu"&gt;### 1. Target Execution &amp;amp; Verification&lt;/span&gt;
To execute commands on the remote target, you must use &lt;span class="sb"&gt;`connect-target.sh`&lt;/span&gt;.
&lt;span class="p"&gt;
*&lt;/span&gt;   &lt;span class="gs"&gt;**Rule:**&lt;/span&gt; Wrap all remote executions in &lt;span class="sb"&gt;`./connect-target.sh "&amp;lt;command&amp;gt;"`&lt;/span&gt;
&lt;span class="p"&gt;*&lt;/span&gt;   &lt;span class="gs"&gt;**Verification:**&lt;/span&gt; Before running complex operations, you must verify the connection and principal by running this exact check:
    &lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;
&lt;/span&gt;
bash
    RESULT=$(./connect-target.sh "echo CONNECTED_OK &amp;amp;&amp;amp; whoami &amp;amp;&amp;amp; hostname")

    if echo "$RESULT" \vert{} grep -q "CONNECTED_OK" &amp;amp;&amp;amp; echo "$RESULT" | grep -q "opuser"; then
      echo "Target reachable, authenticated as opuser."
    else
      echo "Unexpected result, check the output above."
      exit 1
    fi


    &lt;span class="p"&gt;```&lt;/span&gt;
    &lt;span class="ge"&gt;*Do not proceed with further remote commands unless this check passes.*&lt;/span&gt;

&lt;span class="gu"&gt;### 2. Home Assistant API Access&lt;/span&gt;
When interacting with the Home Assistant API, you must retrieve ephemeral credentials via OpenBao.
&lt;span class="p"&gt;
*&lt;/span&gt;   &lt;span class="gs"&gt;**Rule:**&lt;/span&gt; Always source the token via &lt;span class="sb"&gt;`get-ha-token.sh`&lt;/span&gt; to populate the environment variables before making &lt;span class="sb"&gt;`curl`&lt;/span&gt; requests.
&lt;span class="p"&gt;*&lt;/span&gt;   &lt;span class="gs"&gt;**Execution pattern:**&lt;/span&gt;
    &lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;
&lt;/span&gt;
bash
    source ./get-ha-token.sh
    curl -s -H "Authorization: Bearer $HA_TOKEN" "$HA_BASE_URL/api/&amp;lt;endpoint&amp;gt;"


    &lt;span class="p"&gt;```&lt;/span&gt;
&lt;span class="p"&gt;*&lt;/span&gt;   &lt;span class="gs"&gt;**Verification:**&lt;/span&gt; Before executing API changes, verify the token validity and network path by checking the root endpoint for a &lt;span class="sb"&gt;`200`&lt;/span&gt; response:
    &lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;
&lt;/span&gt;
bash
    source ./get-ha-token.sh

    CHECK=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $HA_TOKEN" "$HA_BASE_URL/api/")

    if [ "$CHECK" = "200" ]; then
      echo "Home Assistant reachable, token valid."
    else
      echo "Home Assistant check failed with HTTP $CHECK"
      exit 1
    fi


    &lt;span class="p"&gt;```&lt;/span&gt;
    &lt;span class="ge"&gt;*Do not proceed with Home Assistant API calls unless this check returns HTTP 200. A 401 indicates an OpenBao token issue; a 000 indicates a network/URL issue.*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;connect to home assistant to check total grid import&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;connect to ubuntu target and check which ubuntu version is running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aaaand it works like a charm:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4qap4hw2rtepeb8st2us.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4qap4hw2rtepeb8st2us.png" alt="Claude Code Connecting to Targets via OpenBao from Ubuntu Client" width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude Code was able to access the targets via OpenBao from the Ubuntu client: &lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxq044zko5ejph7lqc9a3.png" alt="Result of Claude Code Connecting to Targets via OpenBao from Ubuntu Client" width="800" height="412"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Same pattern on Windows
&lt;/h2&gt;

&lt;p&gt;Everything above works identically on a Windows machine: same client cert, same OpenBao login, same signed SSH cert or KV read. The one real difference is where the cert lives. Windows has no direct filesystem-permission equivalent, so it goes into the Windows Certificate Store instead of &lt;code&gt;/etc/openbao-client/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Issue and import the cert:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Admin side, same as the Ubuntu issuance, different role/CN&lt;/span&gt;
bao write &lt;span class="nt"&gt;-format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json pki/issue/windows-client &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;common_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"claude-cli-windows"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"8h"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; windows-client-cert.json

jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.data.certificate'&lt;/span&gt; windows-client-cert.json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; client.pem
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.data.private_key'&lt;/span&gt; windows-client-cert.json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; client-key.pem
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.data.issuing_ca'&lt;/span&gt; windows-client-cert.json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; issuing-ca.pem

openssl pkcs12 &lt;span class="nt"&gt;-export&lt;/span&gt; &lt;span class="nt"&gt;-out&lt;/span&gt; claude-cli-windows.pfx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-inkey&lt;/span&gt; client-key.pem &lt;span class="nt"&gt;-in&lt;/span&gt; client.pem &lt;span class="nt"&gt;-certfile&lt;/span&gt; issuing-ca.pem &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-passout&lt;/span&gt; pass:CHANGE_THIS_TEMP_PASSWORD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On the Windows client, PowerShell, not elevated (lands in CurrentUser)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$pfxPassword&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Read-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Prompt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PFX password"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AsSecureString&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Import-PfxCertificate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-FilePath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:\Staging\claude-cli-windows.pfx"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;-CertStoreLocation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Cert:\CurrentUser\My&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$pfxPassword&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Note the thumbprint for the wrapper scripts below&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Cert:\CurrentUser\My&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-cli-windows"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;Import-Certificate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-FilePath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:\Staging\issuing-ca.pem"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-CertStoreLocation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Cert:\CurrentUser\Root&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Remove-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\Staging\claude-cli-windows.pfx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\Staging\client-key.pem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;connect-target.ps1&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="kr"&gt;param&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="bp"&gt;$Error&lt;/span&gt;&lt;span class="n"&gt;ActionPreference&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Stop"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$BaoAddr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://openbao.homelab.local:8200"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$ClientCertThumbprint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PUT_THUMBPRINT_HERE"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$SshRole&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"target-access"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$SshUser&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"opuser"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$TargetHost&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"target.homelab.local"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$KeyDir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;TEMP&lt;/span&gt;&lt;span class="s2"&gt;\bao-ssh-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;guid&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;New-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ItemType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Directory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$KeyDir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-Null&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="c"&gt;# 1. Authenticate&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Invoke-RestMethod&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Uri&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BaoAddr&lt;/span&gt;&lt;span class="s2"&gt;/v1/auth/cert/login"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Method&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Post&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;-CertificateThumbprint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ClientCertThumbprint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client_token&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;throw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OpenBao login did not return a token."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="c"&gt;# 2. Generate Key&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$privKeyPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$KeyDir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"id_ephemeral"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;ssh-keygen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ed25519&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$privKeyPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-N&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'""'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-q&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="c"&gt;# 3. Read the public key, explicitly cast to string, and trim whitespace/newlines&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$pubKey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;Get-Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$privKeyPath&lt;/span&gt;&lt;span class="s2"&gt;.pub"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="c"&gt;# 4. Create the JSON body outside of the Invoke-RestMethod call&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$jsonBody&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;public_key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$pubKey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ConvertTo-Json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Compress&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="c"&gt;# 5. Request Signed Cert&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$signedCert&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Invoke-RestMethod&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Uri&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BaoAddr&lt;/span&gt;&lt;span class="s2"&gt;/v1/ssh/sign/&lt;/span&gt;&lt;span class="nv"&gt;$SshRole&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Method&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Post&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;-Headers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"X-Vault-Token"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;-Body&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$jsonBody&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ContentType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signed_key&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nv"&gt;$certPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$KeyDir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"id_ephemeral-cert.pub"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Set-Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$certPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$signedCert&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-NoNewline&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="c"&gt;# 6. Connect&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ne&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ssh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$privKeyPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$certPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SshUser&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="nv"&gt;$TargetHost&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ssh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$privKeyPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$certPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SshUser&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="nv"&gt;$TargetHost&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;finally&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Remove-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Recurse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$KeyDir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;Get-HaToken.ps1&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$BaoAddr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://openbao.homelab.local:8200"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$ClientCertThumbprint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PUT_THUMBPRINT_HERE"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Invoke-RestMethod&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Uri&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BaoAddr&lt;/span&gt;&lt;span class="s2"&gt;/v1/auth/cert/login"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Method&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Post&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-CertificateThumbprint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ClientCertThumbprint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client_token&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$secret&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Invoke-RestMethod&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Uri&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BaoAddr&lt;/span&gt;&lt;span class="s2"&gt;/v1/homeassistant/data/api"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Method&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Get&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-Headers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"X-Vault-Token"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;HA_TOKEN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$secret&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;token&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;HA_BASE_URL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$secret&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;base_url&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HA_TOKEN and HA_BASE_URL set for this session."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Usage, same verification logic as the Ubuntu scripts:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\connect-target.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"echo CONNECTED_OK &amp;amp;&amp;amp; whoami &amp;amp;&amp;amp; hostname"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\Get-HaToken.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nx"&gt;Invoke-RestMethod&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Uri&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;HA_BASE_URL&lt;/span&gt;&lt;span class="s2"&gt;/api/"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Headers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Authorization&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;HA_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;The verification approach and the &lt;code&gt;CLAUDE.md&lt;/code&gt; wiring match the Ubuntu section above: check for the expected marker/output, point Claude Code at the wrapper scripts instead of raw &lt;code&gt;ssh&lt;/code&gt;/&lt;code&gt;curl&lt;/code&gt; calls with stored credentials.&lt;/p&gt;

&lt;p&gt;My CLAUDE.md&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Windows / PowerShell Operations&lt;/span&gt;

When executing commands on the target machine or interacting with the Home Assistant API in this project, &lt;span class="gs"&gt;**never use raw `ssh` or `curl` commands**&lt;/span&gt;. Always use the provided PowerShell wrapper scripts to handle authentication and routing automatically.

&lt;span class="gu"&gt;### 1. Target Execution &amp;amp; Verification&lt;/span&gt;
To execute commands on the remote target, you must use &lt;span class="sb"&gt;`connect-target.ps1`&lt;/span&gt;.
&lt;span class="p"&gt;
*&lt;/span&gt;   &lt;span class="gs"&gt;**Rule:**&lt;/span&gt; Wrap all remote executions in &lt;span class="sb"&gt;`.\connect-target.ps1 -Command "..."`&lt;/span&gt;
&lt;span class="p"&gt;*&lt;/span&gt;   &lt;span class="gs"&gt;**Verification:**&lt;/span&gt; Before running complex operations, verify the connection by checking for the &lt;span class="sb"&gt;`CONNECTED_OK`&lt;/span&gt; marker:
    &lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;
&lt;/span&gt;
powershell
    .\connect-target.ps1 -Command "echo CONNECTED_OK &amp;amp;&amp;amp; whoami &amp;amp;&amp;amp; hostname"


    &lt;span class="p"&gt;```&lt;/span&gt;
    &lt;span class="ge"&gt;*Do not proceed with further remote commands unless you see `CONNECTED_OK` in the output.*&lt;/span&gt;

&lt;span class="gu"&gt;### 2. Home Assistant API Access&lt;/span&gt;
When interacting with the Home Assistant API, do not attempt to find or use hardcoded credentials. 
&lt;span class="p"&gt;
*&lt;/span&gt;   &lt;span class="gs"&gt;**Rule:**&lt;/span&gt; Always source the token via &lt;span class="sb"&gt;`Get-HaToken.ps1`&lt;/span&gt; to populate the environment variables, then use &lt;span class="sb"&gt;`Invoke-RestMethod`&lt;/span&gt;.
&lt;span class="p"&gt;*&lt;/span&gt;   &lt;span class="gs"&gt;**Execution pattern:**&lt;/span&gt;
    &lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;
&lt;/span&gt;
powershell
    .\Get-HaToken.ps1
    Invoke-RestMethod -Uri "$env:HA_BASE_URL/api/" -Headers @{ Authorization = "Bearer $env:HA_TOKEN" }


    &lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6i2ljikj2b9rir7k2jnp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6i2ljikj2b9rir7k2jnp.png" alt="Claude Code was able to Targets via OpenBao from the Windows client" width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude Code was able to access the targets via OpenBao from the Windows client as well: &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6utk2akzd23x5x71cyhb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6utk2akzd23x5x71cyhb.png" alt="Result of Claude Code Connecting to Targets via OpenBao from Windows Client" width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As my teacher liked to state: &lt;strong&gt;quod erat demonstrandum&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Going further: transit auto-unseal and root token hygiene
&lt;/h2&gt;

&lt;p&gt;Everything above gives a fully working setup with one deliberate manual step: unsealing after a restart. That is sufficient for most home labs. If the server needs to survive an unattended reboot without you physically present, for example an agent depends on it continuously and a stalled task should not wait on you to notice, remove that manual step by adding a &lt;strong&gt;second&lt;/strong&gt; OpenBao instance whose only job is holding an unseal key via the &lt;strong&gt;transit&lt;/strong&gt; secrets engine. This is additive: nothing above changes, this inserts a helper node upstream of the server already built.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; instead of Shamir shares, the primary server's &lt;code&gt;bao.hcl&lt;/code&gt; gets a &lt;code&gt;seal "transit"&lt;/code&gt; stanza pointing at a second, separate OpenBao instance, the "unseal node". On every start, the primary calls the unseal node's transit engine to decrypt its master key automatically, no &lt;code&gt;bao operator unseal&lt;/code&gt; required. The trade-off: the unseal node itself still needs something to unseal it, typically Shamir, kept manual since it restarts far less often than the primary, and it becomes a hard availability dependency. If it is down or unreachable, the primary will not come up after a restart even though it is otherwise healthy.&lt;/p&gt;

&lt;p&gt;There are plenty of guides out there how to set this up.&lt;/p&gt;
&lt;h3&gt;
  
  
  Proxmox snapshots and Raft state
&lt;/h3&gt;

&lt;p&gt;If the OpenBao server runs as a Proxmox VM, as this guide assumes, do not roll back a live Proxmox snapshot of that VM while OpenBao is running. Raft storage keeps a consistency log on disk, and Proxmox's snapshot mechanism has no awareness of that log or the need to keep it consistent with OpenBao's own state. Reverting to an older snapshot can leave Raft's log pointing at a state that no longer matches reality, which shows up as anything from OpenBao refusing to start to confusing data inconsistencies, the kind of failure that is hard to debug because it looks like OpenBao is broken rather than the storage underneath it.&lt;/p&gt;

&lt;p&gt;For a safe rollback point, stop the &lt;code&gt;openbao&lt;/code&gt; service first, before taking or restoring a Proxmox snapshot. Better: do not rely on hypervisor snapshots for OpenBao backups at all. Use Raft's own snapshot mechanism, which stays consistent with what Raft actually knows about its own state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bao operator raft snapshot save backup-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;.snap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restoring from one, only for troubleshooting, since it replaces current state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bao operator raft snapshot restore backup-20260101.snap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use Proxmox snapshots for what they are good at: disaster recovery of the whole VM if something below the OS breaks. Use &lt;code&gt;bao operator raft snapshot save&lt;/code&gt; for OpenBao state backups. The same logic applies when moving from a single node to a multi-node Raft cluster: an inconsistent snapshot restore on one node while the others keep running is the standard way to end up with split-brain, where nodes disagree about the current state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retiring the root token
&lt;/h3&gt;

&lt;p&gt;Once the policies in this guide exist (&lt;code&gt;ssh-target-sign&lt;/code&gt;, &lt;code&gt;homeassistant-read&lt;/code&gt;, &lt;code&gt;pki-issue-admin&lt;/code&gt;, and cert-auth registrations), the root token has done its job and should not remain a standing credential. Revoke it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bao token revoke &lt;span class="nt"&gt;-self&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For root-equivalent access again later, adding a new secrets engine, changing a policy structure, or anything else the guide's scoped policies do not cover, generate a new one on demand instead of keeping one permanently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bao operator generate-root &lt;span class="nt"&gt;-init&lt;/span&gt;
&lt;span class="c"&gt;# follow the prompts, this walks you through combining key shares&lt;/span&gt;
&lt;span class="c"&gt;# (the same ones from the KeePassXC entries above) to mint a fresh root token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  You have reached the end of this article, thank you for your attention (or if you have been impatient: just read my tl;dr at the top) :-)
&lt;/h2&gt;

</description>
      <category>openbao</category>
      <category>aiops</category>
      <category>security</category>
      <category>identity</category>
    </item>
  </channel>
</rss>
