<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>GitGuardian on IAMDevBox</title><link>https://www.iamdevbox.com/tags/gitguardian/</link><description>Recent content in GitGuardian on IAMDevBox</description><image><title>IAMDevBox</title><url>https://www.iamdevbox.com/IAMDevBox.com.jpg</url><link>https://www.iamdevbox.com/IAMDevBox.com.jpg</link></image><generator>Hugo -- 0.146.0</generator><language>en-us</language><lastBuildDate>Thu, 16 Apr 2026 19:55:55 -0400</lastBuildDate><atom:link href="https://www.iamdevbox.com/tags/gitguardian/index.xml" rel="self" type="application/rss+xml"/><item><title>NHI Secrets Sprawl: How to Fix the Non-Human Identity Credential Crisis</title><link>https://www.iamdevbox.com/posts/nhi-secrets-sprawl-fixing-the-non-human-identity-credential-crisis/</link><pubDate>Thu, 16 Apr 2026 19:55:00 +0000</pubDate><guid>https://www.iamdevbox.com/posts/nhi-secrets-sprawl-fixing-the-non-human-identity-credential-crisis/</guid><description>GitGuardian&amp;#39;s 2026 report found 29M secrets on public GitHub — AI service credentials surged 81% YoY. Here&amp;#39;s how to eliminate NHI secrets sprawl with rotation, vaulting, and workload identity federation.</description><content:encoded><![CDATA[<p>GitGuardian&rsquo;s <em>State of Secrets Sprawl 2026</em> report landed with a jarring finding: <strong>29 million secrets</strong> were detected on public GitHub in the past year alone. More alarming — credentials for AI services (OpenAI, Anthropic, Hugging Face, Cohere) surged <strong>81% year-over-year</strong>, driven by developers rushing to integrate LLMs without applying the same discipline they&rsquo;d use for database passwords. And 64% of secrets exposed in 2022 were <em>still valid and unrevoked</em> in 2025.</p>
<p>This is the NHI (Non-Human Identity) secrets sprawl problem: credentials used by machines, not humans, accumulating across repos, CI/CD systems, Kubernetes clusters, and developer laptops — without central governance, rotation, or auditability.</p>
<p>For a broader look at how workload identity eliminates static keys entirely, see our <a href="/posts/go-secretless-with-snowflake-workload-identity-federation-snowflake/">Workload Identity Federation guide</a> and the companion <a href="/posts/service-account-security-best-practices-for-api-and-microservice-authentication/">Service Account Security best practices</a>.</p>
<h2 id="why-nhi-secrets-are-different-from-human-credentials">Why NHI Secrets Are Different from Human Credentials</h2>
<p>Human credentials (passwords, MFA) have mature lifecycle management: directory integration, password policies, MFA enforcement, session timeouts. When a human employee offboards, their accounts are disabled within hours.</p>
<p>NHI credentials — service account keys, API tokens, database passwords embedded in application configs, CI/CD secrets — lack this governance:</p>
<table>
  <thead>
      <tr>
          <th>Property</th>
          <th>Human Credentials</th>
          <th>NHI Credentials</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Owner</td>
          <td>Named person</td>
          <td>Often &ldquo;the team&rdquo; or &ldquo;devops&rdquo;</td>
      </tr>
      <tr>
          <td>Offboarding</td>
          <td>Automated via directory</td>
          <td>Usually manual and forgotten</td>
      </tr>
      <tr>
          <td>Rotation</td>
          <td>Password policy enforced</td>
          <td>Often months or years</td>
      </tr>
      <tr>
          <td>MFA</td>
          <td>Standard</td>
          <td>Rare (and often impossible)</td>
      </tr>
      <tr>
          <td>Audit trail</td>
          <td>Login events, SSO logs</td>
          <td>Scattered — API call logs if enabled</td>
      </tr>
      <tr>
          <td>Detection if compromised</td>
          <td>Behavioral analytics, UEBA</td>
          <td>Often discovered in breach postmortems</td>
      </tr>
  </tbody>
</table>
<p>The GitGuardian 2026 data surfaces a specific failure mode: the <strong>AI development acceleration</strong> is creating a new wave of NHI credentials with even less governance. Developers prototype with an OpenAI key committed to a <code>.env</code> file, ship fast, and never clean it up. The key persists in git history, CI/CD environment variables, Docker image layers, and Slack messages for years.</p>
<h2 id="the-four-layers-where-nhi-secrets-accumulate">The Four Layers Where NHI Secrets Accumulate</h2>
<h3 id="1-source-code-and-git-history">1. Source Code and Git History</h3>
<p>The most common exposure vector. <code>git log --all -p | grep -E &quot;sk-|AKIA|AIza&quot;</code> often reveals secrets committed years ago in what developers thought was a temporary experiment. Git history is permanent — even after a file deletion commit, tools like <code>git filter-repo</code> are needed to purge secrets from all branches and tags.</p>
<p><strong>Detection</strong>: <a href="https://docs.github.com/en/code-security/secret-scanning">GitHub Secret Scanning</a> (free for public repos, included in GHAS for private), GitGuardian, TruffleHog.</p>
<p><strong>Remediation checklist</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Find secrets in current working tree</span>
</span></span><span style="display:flex;"><span>trufflehog git file://. --only-verified
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Find secrets in full history</span>
</span></span><span style="display:flex;"><span>trufflehog git file://. --since-commit<span style="color:#f92672">=</span>HEAD~100
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Purge from history (destructive — requires force push)</span>
</span></span><span style="display:flex;"><span>git filter-repo --invert-paths --path config/secrets.yaml
</span></span></code></pre></div><h3 id="2-cicd-environment-variables">2. CI/CD Environment Variables</h3>
<p>GitHub Actions, GitLab CI, Jenkins, CircleCI, and Bitbucket Pipelines all support secrets/environment variables — but their storage and access controls vary enormously. Common misconfigurations:</p>
<ul>
<li>Secrets available to all branches including forks (GitHub: use environment protection rules)</li>
<li>Secrets logged to build output via <code>echo $SECRET</code> or framework debug modes</li>
<li>Secrets stored in CI config YAML alongside source code (<code>env: AWS_SECRET: mypassword</code>)</li>
</ul>
<p><strong>Better pattern</strong> — use OIDC-based workload identity instead of static keys:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># GitHub Actions: authenticate to AWS without any static key</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">aws-actions/configure-aws-credentials@v4</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">role-to-assume</span>: <span style="color:#ae81ff">arn:aws:iam::123456789012:role/GitHubActionsRole</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">aws-region</span>: <span style="color:#ae81ff">us-east-1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># No aws-access-key-id or aws-secret-access-key needed</span>
</span></span></code></pre></div><p>This eliminates the AWS key entirely. The OIDC token from GitHub&rsquo;s identity provider is verified by AWS STS and exchanged for short-lived session credentials.</p>
<h3 id="3-kubernetes-secrets-and-container-images">3. Kubernetes Secrets and Container Images</h3>
<p>Kubernetes Secrets are base64-encoded (not encrypted) by default. Any pod in the same namespace that can <code>kubectl get secret</code> will read every secret there. Common mistakes:</p>
<ul>
<li>Mounting secrets as environment variables (visible in <code>/proc/1/environ</code> on a compromised container)</li>
<li>Baking secrets into container images via <code>ENV</code> directives</li>
<li>Using default service accounts with overly permissive RBAC</li>
</ul>
<p><strong>Better pattern</strong> — use External Secrets Operator to sync from a real vault:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">external-secrets.io/v1beta1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ExternalSecret</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">database-credentials</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">refreshInterval</span>: <span style="color:#ae81ff">1h</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">secretStoreRef</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">vault-backend</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ClusterSecretStore</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">target</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">db-credentials</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">creationPolicy</span>: <span style="color:#ae81ff">Owner</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">data</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">secretKey</span>: <span style="color:#ae81ff">password</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">remoteRef</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">key</span>: <span style="color:#ae81ff">secret/data/prod/database</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">property</span>: <span style="color:#ae81ff">password</span>
</span></span></code></pre></div><p>The vault (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager) holds the secret with rotation, audit logging, and lease expiration. The Kubernetes Secret is a cache — ephemeral and auto-refreshed.</p>
<h3 id="4-cloud-configuration-and-iac">4. Cloud Configuration and IaC</h3>
<p>Terraform state files, AWS CloudFormation templates, and Helm chart values files routinely contain sensitive values. Terraform state (<code>terraform.tfstate</code>) is particularly dangerous — it stores all resource configurations including any credentials passed as variables.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#75715e"># WRONG: hardcoded secret in Terraform
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_db_instance&#34; &#34;main&#34;</span> {
</span></span><span style="display:flex;"><span>  password <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;MySecretPassword123!&#34;</span><span style="color:#75715e">  # This ends up in terraform.tfstate
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># BETTER: reference from AWS Secrets Manager
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;aws_secretsmanager_secret_version&#34; &#34;db_password&#34;</span> {
</span></span><span style="display:flex;"><span>  secret_id <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;prod/rds/password&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_db_instance&#34; &#34;main&#34;</span> {
</span></span><span style="display:flex;"><span>  password <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">aws_secretsmanager_secret_version</span>.<span style="color:#66d9ef">db_password</span>.<span style="color:#66d9ef">secret_string</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Store Terraform state in encrypted S3 with versioning and access logging. Use <code>terraform.tfstate</code> backend encryption with KMS CMK.</p>
<h2 id="the-64-problem-detection-without-remediation">The 64% Problem: Detection Without Remediation</h2>
<p>The most alarming GitGuardian finding isn&rsquo;t the discovery rate — it&rsquo;s the remediation rate. 64% of secrets exposed in 2022 were still valid three years later.</p>
<p>This reveals a broken incident response process:</p>
<ol>
<li>Secret is detected (by GitGuardian, a security scanner, or a breach notification)</li>
<li>Issue is filed in the tracker</li>
<li>Developer claims &ldquo;I&rsquo;ll rotate it later&rdquo;</li>
<li>Later never comes</li>
</ol>
<h3 id="forcing-function-automated-rotation">Forcing Function: Automated Rotation</h3>
<p>Break the &ldquo;rotate later&rdquo; cycle by making rotation automatic:</p>
<p><strong>AWS Secrets Manager</strong> — built-in rotation with Lambda functions:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>aws secretsmanager rotate-secret <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --secret-id prod/database/credentials <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --rotation-rules AutomaticallyAfterDays<span style="color:#f92672">=</span><span style="color:#ae81ff">30</span>
</span></span></code></pre></div><p><strong>HashiCorp Vault</strong> — dynamic secrets (credentials generated on-demand with automatic expiration):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Vault generates a temporary database credential valid for 1 hour</span>
</span></span><span style="display:flex;"><span>vault read database/creds/readonly-role
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Key: v8YdtAY7GVaKBYP2-BVi3S</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Password: A1a-... (valid 1h, then auto-revoked)</span>
</span></span></code></pre></div><p><strong>GCP Secret Manager</strong> — combined with Workload Identity, no rotation needed:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Instead of rotating a key, eliminate the key entirely</span>
</span></span><span style="display:flex;"><span>gcloud iam service-accounts delete old-service-account@project.iam.gserviceaccount.com
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Workload uses its OIDC identity — nothing to rotate</span>
</span></span></code></pre></div><h2 id="nhi-secrets-sprawl-remediation-roadmap">NHI Secrets Sprawl Remediation Roadmap</h2>
<h3 id="week-1-inventory">Week 1: Inventory</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Scan all repos in your GitHub org</span>
</span></span><span style="display:flex;"><span>docker run --rm -e GITHUB_TOKEN<span style="color:#f92672">=</span>$GITHUB_TOKEN <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  trufflesecurity/trufflehog:latest <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  github --org<span style="color:#f92672">=</span>your-org --only-verified
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Check cloud service accounts for old keys</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># GCP:</span>
</span></span><span style="display:flex;"><span>gcloud iam service-accounts list --format<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;value(email)&#34;</span> | <span style="color:#66d9ef">while</span> read SA; <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  gcloud iam service-accounts keys list --iam-account<span style="color:#f92672">=</span>$SA <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    --filter<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;keyType=USER_MANAGED&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    --format<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;value(name,validAfterTime)&#34;</span> | <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    awk -v sa<span style="color:#f92672">=</span>$SA <span style="color:#e6db74">&#39;{print sa, $0}&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">done</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># AWS:</span>
</span></span><span style="display:flex;"><span>aws iam generate-credential-report
</span></span><span style="display:flex;"><span>aws iam get-credential-report --query <span style="color:#e6db74">&#39;Content&#39;</span> --output text | base64 -d | <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  awk -F, <span style="color:#e6db74">&#39;$9 &gt; 90 {print $1, &#34;access key&#34;, $9, &#34;days old&#34;}&#39;</span>
</span></span></code></pre></div><h3 id="week-2-stop-the-bleeding">Week 2: Stop the Bleeding</h3>
<ul>
<li>Enable pre-commit hooks with <code>detect-secrets</code> or <code>git-secrets</code> for all new commits</li>
<li>Add GitHub&rsquo;s built-in secret scanning to all private repositories</li>
<li>Set up branch protection rules so PRs with detected secrets can&rsquo;t merge</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Install detect-secrets pre-commit hook</span>
</span></span><span style="display:flex;"><span>pip install detect-secrets
</span></span><span style="display:flex;"><span>detect-secrets scan &gt; .secrets.baseline
</span></span><span style="display:flex;"><span>cat &gt; .pre-commit-config.yaml <span style="color:#e6db74">&lt;&lt; &#39;EOF&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">repos:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">- repo: https://github.com/Yelp/detect-secrets
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  rev: v1.4.0
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  hooks:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  - id: detect-secrets
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    args: [&#39;--baseline&#39;, &#39;.secrets.baseline&#39;]
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">EOF</span>
</span></span><span style="display:flex;"><span>pre-commit install
</span></span></code></pre></div><h3 id="week-3-vault-everything-new">Week 3: Vault Everything New</h3>
<p>For all new secrets, use a vault from day one. No exceptions for &ldquo;just a test&rdquo; or &ldquo;temporary&rdquo; credentials. Tests secrets are production secrets waiting to happen.</p>
<h3 id="weeks-4-12-migrate-existing-secrets">Weeks 4-12: Migrate Existing Secrets</h3>
<p>Prioritize by impact:</p>
<ol>
<li><strong>P0</strong>: Any secret with confirmed exposure in git history → rotate immediately, no exceptions</li>
<li><strong>P1</strong>: Production cloud provider keys (AWS, GCP, Azure) → migrate to Workload Identity or Secrets Manager</li>
<li><strong>P2</strong>: CI/CD secrets → migrate to OIDC-based workload identity</li>
<li><strong>P3</strong>: Internal service-to-service credentials → migrate to mTLS or service mesh identity</li>
</ol>
<h3 id="ongoing-measure-nhi-hygiene">Ongoing: Measure NHI Hygiene</h3>
<p>Track these metrics monthly:</p>
<ul>
<li><strong>Static key age</strong>: % of service account keys older than 90 days (target: 0%)</li>
<li><strong>Secrets in vault</strong>: % of NHI secrets managed through a vault (target: 100%)</li>
<li><strong>Mean time to rotate</strong> after detected exposure (target: &lt; 4 hours)</li>
<li><strong>OIDC-native workloads</strong>: % of CI/CD pipelines using workload identity instead of static keys</li>
</ul>
<h2 id="ai-credential-leaks-the-2026-specific-problem">AI Credential Leaks: The 2026 Specific Problem</h2>
<p>The 81% surge in AI service credentials on GitHub deserves specific treatment. Unlike AWS or GCP keys (which have IAM policies limiting blast radius), leaked OpenAI or Anthropic API keys carry:</p>
<ul>
<li><strong>Unbounded financial exposure</strong>: No granular resource controls — a leaked key can burn thousands in API calls in hours</li>
<li><strong>Data exfiltration risk</strong>: API calls send your prompts to the provider&rsquo;s infrastructure</li>
<li><strong>Attribution loss</strong>: You can&rsquo;t tell which calls were legitimate vs. attacker abuse from API logs alone</li>
</ul>
<p><strong>Controls for AI service credentials</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># WRONG: hardcoded API key</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> openai
</span></span><span style="display:flex;"><span>client <span style="color:#f92672">=</span> openai<span style="color:#f92672">.</span>OpenAI(api_key<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz...&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># BETTER: environment variable (still leaks to process environ — not ideal)</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> os
</span></span><span style="display:flex;"><span>client <span style="color:#f92672">=</span> openai<span style="color:#f92672">.</span>OpenAI(api_key<span style="color:#f92672">=</span>os<span style="color:#f92672">.</span>environ[<span style="color:#e6db74">&#34;OPENAI_API_KEY&#34;</span>])
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># BEST: pull from vault at runtime with short-lived lease</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> hvac
</span></span><span style="display:flex;"><span>vault <span style="color:#f92672">=</span> hvac<span style="color:#f92672">.</span>Client(url<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://vault.example.com&#34;</span>, token<span style="color:#f92672">=</span>os<span style="color:#f92672">.</span>environ[<span style="color:#e6db74">&#34;VAULT_TOKEN&#34;</span>])
</span></span><span style="display:flex;"><span>secret <span style="color:#f92672">=</span> vault<span style="color:#f92672">.</span>secrets<span style="color:#f92672">.</span>kv<span style="color:#f92672">.</span>v2<span style="color:#f92672">.</span>read_secret_version(path<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;ai-services/openai&#34;</span>)
</span></span><span style="display:flex;"><span>client <span style="color:#f92672">=</span> openai<span style="color:#f92672">.</span>OpenAI(api_key<span style="color:#f92672">=</span>secret[<span style="color:#e6db74">&#34;data&#34;</span>][<span style="color:#e6db74">&#34;data&#34;</span>][<span style="color:#e6db74">&#34;api_key&#34;</span>])
</span></span></code></pre></div><p>Additionally, set <strong>spending limits</strong> on AI provider accounts as a circuit breaker — not a security control, but a blast radius limiter.</p>
<h2 id="the-path-to-zero-static-nhi-secrets">The Path to Zero Static NHI Secrets</h2>
<p>The end state is architecturally simple but operationally hard: <strong>no long-lived static credentials anywhere</strong>. Every NHI authenticates using one of:</p>
<ol>
<li><strong>Workload identity federation</strong> (for cloud provider APIs from CI/CD or compute)</li>
<li><strong>mTLS with short-lived certs</strong> (for service-to-service within your infrastructure, issued by an internal CA like SPIFFE/SPIRE)</li>
<li><strong>Dynamic secrets from vault</strong> (for databases, legacy systems that can&rsquo;t accept OIDC tokens)</li>
</ol>
<p>The GitGuardian 2026 data tells us we&rsquo;re far from that end state. But the tooling now exists to get there — the gap is organizational discipline, not technical capability. Start with your public-facing repos, move to CI/CD pipelines, then tackle the long tail of internal service credentials.</p>
<p>For implementation details on eliminating service account keys with workload identity federation, see our <a href="/posts/service-account-security-best-practices-for-api-and-microservice-authentication/">Service Account Security guide</a>. For Kubernetes-specific NHI patterns, our <a href="/posts/orchestrating-kubernetes-and-iam-with-terraform-a-comprehensive-guide/">IRSA and Workload Identity article</a> covers the full Terraform implementation.</p>
<hr>
<p><em>Related tools: <a href="/tools/jwt-decoder/">JWT Decoder</a> to inspect token claims from workload identity flows. <a href="/tools/oauth-playground/">OAuth Playground</a> to test OIDC-based workload identity token exchange.</em></p>
]]></content:encoded></item></channel></rss>