GitGuardian’s State of Secrets Sprawl 2026 report landed with a jarring finding: 29 million secrets were detected on public GitHub in the past year alone. More alarming — credentials for AI services (OpenAI, Anthropic, Hugging Face, Cohere) surged 81% year-over-year, driven by developers rushing to integrate LLMs without applying the same discipline they’d use for database passwords. And 64% of secrets exposed in 2022 were still valid and unrevoked in 2025.

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.

For a broader look at how workload identity eliminates static keys entirely, see our Workload Identity Federation guide and the companion Service Account Security best practices.

Why NHI Secrets Are Different from Human Credentials

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.

NHI credentials — service account keys, API tokens, database passwords embedded in application configs, CI/CD secrets — lack this governance:

PropertyHuman CredentialsNHI Credentials
OwnerNamed personOften “the team” or “devops”
OffboardingAutomated via directoryUsually manual and forgotten
RotationPassword policy enforcedOften months or years
MFAStandardRare (and often impossible)
Audit trailLogin events, SSO logsScattered — API call logs if enabled
Detection if compromisedBehavioral analytics, UEBAOften discovered in breach postmortems

The GitGuardian 2026 data surfaces a specific failure mode: the AI development acceleration is creating a new wave of NHI credentials with even less governance. Developers prototype with an OpenAI key committed to a .env 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.

The Four Layers Where NHI Secrets Accumulate

1. Source Code and Git History

The most common exposure vector. git log --all -p | grep -E "sk-|AKIA|AIza" 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 git filter-repo are needed to purge secrets from all branches and tags.

Detection: GitHub Secret Scanning (free for public repos, included in GHAS for private), GitGuardian, TruffleHog.

Remediation checklist:

# Find secrets in current working tree
trufflehog git file://. --only-verified

# Find secrets in full history
trufflehog git file://. --since-commit=HEAD~100

# Purge from history (destructive — requires force push)
git filter-repo --invert-paths --path config/secrets.yaml

2. CI/CD Environment Variables

GitHub Actions, GitLab CI, Jenkins, CircleCI, and Bitbucket Pipelines all support secrets/environment variables — but their storage and access controls vary enormously. Common misconfigurations:

  • Secrets available to all branches including forks (GitHub: use environment protection rules)
  • Secrets logged to build output via echo $SECRET or framework debug modes
  • Secrets stored in CI config YAML alongside source code (env: AWS_SECRET: mypassword)

Better pattern — use OIDC-based workload identity instead of static keys:

# GitHub Actions: authenticate to AWS without any static key
- uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
    aws-region: us-east-1
    # No aws-access-key-id or aws-secret-access-key needed

This eliminates the AWS key entirely. The OIDC token from GitHub’s identity provider is verified by AWS STS and exchanged for short-lived session credentials.

3. Kubernetes Secrets and Container Images

Kubernetes Secrets are base64-encoded (not encrypted) by default. Any pod in the same namespace that can kubectl get secret will read every secret there. Common mistakes:

  • Mounting secrets as environment variables (visible in /proc/1/environ on a compromised container)
  • Baking secrets into container images via ENV directives
  • Using default service accounts with overly permissive RBAC

Better pattern — use External Secrets Operator to sync from a real vault:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: database-credentials
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend
    kind: ClusterSecretStore
  target:
    name: db-credentials
    creationPolicy: Owner
  data:
  - secretKey: password
    remoteRef:
      key: secret/data/prod/database
      property: password

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.

4. Cloud Configuration and IaC

Terraform state files, AWS CloudFormation templates, and Helm chart values files routinely contain sensitive values. Terraform state (terraform.tfstate) is particularly dangerous — it stores all resource configurations including any credentials passed as variables.

# WRONG: hardcoded secret in Terraform
resource "aws_db_instance" "main" {
  password = "MySecretPassword123!"  # This ends up in terraform.tfstate
}

# BETTER: reference from AWS Secrets Manager
data "aws_secretsmanager_secret_version" "db_password" {
  secret_id = "prod/rds/password"
}

resource "aws_db_instance" "main" {
  password = data.aws_secretsmanager_secret_version.db_password.secret_string
}

Store Terraform state in encrypted S3 with versioning and access logging. Use terraform.tfstate backend encryption with KMS CMK.

The 64% Problem: Detection Without Remediation

The most alarming GitGuardian finding isn’t the discovery rate — it’s the remediation rate. 64% of secrets exposed in 2022 were still valid three years later.

This reveals a broken incident response process:

  1. Secret is detected (by GitGuardian, a security scanner, or a breach notification)
  2. Issue is filed in the tracker
  3. Developer claims “I’ll rotate it later”
  4. Later never comes

Forcing Function: Automated Rotation

Break the “rotate later” cycle by making rotation automatic:

AWS Secrets Manager — built-in rotation with Lambda functions:

aws secretsmanager rotate-secret \
  --secret-id prod/database/credentials \
  --rotation-rules AutomaticallyAfterDays=30

HashiCorp Vault — dynamic secrets (credentials generated on-demand with automatic expiration):

# Vault generates a temporary database credential valid for 1 hour
vault read database/creds/readonly-role
# Key: v8YdtAY7GVaKBYP2-BVi3S
# Password: A1a-... (valid 1h, then auto-revoked)

GCP Secret Manager — combined with Workload Identity, no rotation needed:

# Instead of rotating a key, eliminate the key entirely
gcloud iam service-accounts delete old-service-account@project.iam.gserviceaccount.com
# Workload uses its OIDC identity — nothing to rotate

NHI Secrets Sprawl Remediation Roadmap

Week 1: Inventory

# Scan all repos in your GitHub org
docker run --rm -e GITHUB_TOKEN=$GITHUB_TOKEN \
  trufflesecurity/trufflehog:latest \
  github --org=your-org --only-verified

# Check cloud service accounts for old keys
# GCP:
gcloud iam service-accounts list --format="value(email)" | while read SA; do
  gcloud iam service-accounts keys list --iam-account=$SA \
    --filter="keyType=USER_MANAGED" \
    --format="value(name,validAfterTime)" | \
    awk -v sa=$SA '{print sa, $0}'
done

# AWS:
aws iam generate-credential-report
aws iam get-credential-report --query 'Content' --output text | base64 -d | \
  awk -F, '$9 > 90 {print $1, "access key", $9, "days old"}'

Week 2: Stop the Bleeding

  • Enable pre-commit hooks with detect-secrets or git-secrets for all new commits
  • Add GitHub’s built-in secret scanning to all private repositories
  • Set up branch protection rules so PRs with detected secrets can’t merge
# Install detect-secrets pre-commit hook
pip install detect-secrets
detect-secrets scan > .secrets.baseline
cat > .pre-commit-config.yaml << 'EOF'
repos:
- repo: https://github.com/Yelp/detect-secrets
  rev: v1.4.0
  hooks:
  - id: detect-secrets
    args: ['--baseline', '.secrets.baseline']
EOF
pre-commit install

Week 3: Vault Everything New

For all new secrets, use a vault from day one. No exceptions for “just a test” or “temporary” credentials. Tests secrets are production secrets waiting to happen.

Weeks 4-12: Migrate Existing Secrets

Prioritize by impact:

  1. P0: Any secret with confirmed exposure in git history → rotate immediately, no exceptions
  2. P1: Production cloud provider keys (AWS, GCP, Azure) → migrate to Workload Identity or Secrets Manager
  3. P2: CI/CD secrets → migrate to OIDC-based workload identity
  4. P3: Internal service-to-service credentials → migrate to mTLS or service mesh identity

Ongoing: Measure NHI Hygiene

Track these metrics monthly:

  • Static key age: % of service account keys older than 90 days (target: 0%)
  • Secrets in vault: % of NHI secrets managed through a vault (target: 100%)
  • Mean time to rotate after detected exposure (target: < 4 hours)
  • OIDC-native workloads: % of CI/CD pipelines using workload identity instead of static keys

AI Credential Leaks: The 2026 Specific Problem

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:

  • Unbounded financial exposure: No granular resource controls — a leaked key can burn thousands in API calls in hours
  • Data exfiltration risk: API calls send your prompts to the provider’s infrastructure
  • Attribution loss: You can’t tell which calls were legitimate vs. attacker abuse from API logs alone

Controls for AI service credentials:

# WRONG: hardcoded API key
import openai
client = openai.OpenAI(api_key="sk-proj-AbCdEfGhIjKlMnOpQrStUvWxYz...")

# BETTER: environment variable (still leaks to process environ — not ideal)
import os
client = openai.OpenAI(api_key=os.environ["OPENAI_API_KEY"])

# BEST: pull from vault at runtime with short-lived lease
import hvac
vault = hvac.Client(url="https://vault.example.com", token=os.environ["VAULT_TOKEN"])
secret = vault.secrets.kv.v2.read_secret_version(path="ai-services/openai")
client = openai.OpenAI(api_key=secret["data"]["data"]["api_key"])

Additionally, set spending limits on AI provider accounts as a circuit breaker — not a security control, but a blast radius limiter.

The Path to Zero Static NHI Secrets

The end state is architecturally simple but operationally hard: no long-lived static credentials anywhere. Every NHI authenticates using one of:

  1. Workload identity federation (for cloud provider APIs from CI/CD or compute)
  2. mTLS with short-lived certs (for service-to-service within your infrastructure, issued by an internal CA like SPIFFE/SPIRE)
  3. Dynamic secrets from vault (for databases, legacy systems that can’t accept OIDC tokens)

The GitGuardian 2026 data tells us we’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.

For implementation details on eliminating service account keys with workload identity federation, see our Service Account Security guide. For Kubernetes-specific NHI patterns, our IRSA and Workload Identity article covers the full Terraform implementation.


Related tools: JWT Decoder to inspect token claims from workload identity flows. OAuth Playground to test OIDC-based workload identity token exchange.