K0KEYZERO
← All posts

From Hardcoded Secrets to Zero-Knowledge: A Migration Path

A step-by-step guide to migrating from hardcoded secrets to vault-backed, policy-controlled secret resolution with KeyZero

·migration, getting-started, best-practices

The Migration Problem

Most codebases accumulate secrets organically. An API key starts in a .env file, gets copied to CI config, shows up in a Docker Compose override, and eventually lands in a Slack message. Migrating away from this pattern requires a systematic approach: audit, centralize, abstract, isolate, and enforce.

KeyZero provides the tooling for each stage. This guide walks through the full migration path, from finding hardcoded secrets to running AI agents in blind mode with policy-controlled access.

Audit Phase: Finding All Secrets in Your Codebase

Before migrating, you need a complete inventory. Secrets hide in predictable places:

  • .env files -- the most common location. Check .env, .env.local, .env.production, and any variant.
  • CI/CD configuration -- GitHub Actions secrets, GitLab CI variables, Jenkins credentials.
  • Docker and Compose files -- environment blocks, build args, mounted secret files.
  • Application code -- hardcoded strings in config files, test fixtures, seed scripts.
  • Shell history and scripts -- export statements, curl commands with API keys.

Use tools like trufflehog, gitleaks, or detect-secrets to scan your repository history. The goal is a list of every secret, where it lives, and what it authenticates against.

Step 1: Move Secrets to a Vault Provider

Pick a vault backend that fits your infrastructure. KeyZero supports five backend types out of the box:

BackendBest ForTOML Provider / Bundle Type
macOS KeychainLocal developmentkeychain
1Password CLITeam workflows1password / onepassword_cli
HashiCorp VaultProduction infrastructurehashicorp_vault
AWS Secrets ManagerAWS-native workloadsaws_secrets_manager
Environment fileQuick testingenv_file

For a typical team, start with macOS Keychain or 1Password for local development and AWS Secrets Manager or HashiCorp Vault for production. Store each secret in your chosen backend with a clear naming convention (e.g., myapp/prod/database-url).

Step 2: Create a .keyzero.toml Configuration

The .keyzero.toml file maps environment variable names to provider references. Place it in your project root:

[secrets]
DATABASE_URL = { provider = "keychain", ref = "myapp-db-url" }
API_KEY      = { provider = "1password", ref = "op://Engineering/myapp/api-key" }
STRIPE_KEY   = { provider = "keychain", ref = "myapp-stripe-key" }

[production.secrets]
DATABASE_URL = { provider = "aws", ref = "sm://prod/myapp/db-url" }
API_KEY      = { provider = "aws", ref = "sm://prod/myapp/api-key" }
STRIPE_KEY   = { provider = "aws", ref = "sm://prod/myapp/stripe-key" }

The [secrets] section is the default. Set KEYZERO_ENV=production to activate [production.secrets], which overrides the defaults. This lets developers use Keychain locally while production uses AWS Secrets Manager.

For personal overrides, create .keyzero.local.toml (add it to .gitignore):

[secrets]
DATABASE_URL = { provider = "keychain", ref = "my-personal-db-url" }

Step 3: Replace Direct References with kz run

Replace direct secret injection with kz run. This is the core shift from static files to runtime secret resolution:

Before:

export DATABASE_URL="postgres://user:password@host:5432/db"
npm start

After:

kz run -- npm start

KeyZero resolves each secret from its configured provider and injects it as an environment variable. The subprocess sees the same DATABASE_URL environment variable -- no application code changes required.

For CI/CD, the same pattern applies. In a GitHub Actions workflow:

- name: Run with secrets
  run: kz run -- npm test
  env:
    KEYZERO_ENV: ci

Shell Hooks for Automatic Resolution

For development workflows, install the shell hook so secrets load automatically when you cd into a project:

# zsh
eval "$(kz hook --shell zsh)"

# bash
eval "$(kz hook --shell bash)"

# fish
kz hook --shell fish | source

Step 4: Enable Blind Mode for AI Workloads

AI agents are the highest-risk consumers of secrets. They copy values into context windows, include them in generated code, and send them to external APIs. Blind mode eliminates this risk.

kz run --blind -- node agent.js

In blind mode, the subprocess sees masked tokens (kz_masked_7f3a9b...) instead of real secret values. A local MITM proxy intercepts outgoing HTTP/HTTPS requests and swaps the masked tokens for real credentials at the network boundary. The agent can make authenticated API calls without ever seeing the actual credentials.

Control which hosts the agent can reach:

[blind]
[[blind.connections]]
allow = true
host = "api.openai.com"

[[blind.connections]]
allow = false
host = "*"

This restricts the agent to only the APIs it needs, preventing credential exfiltration to unauthorized endpoints.

Step 5: Add Policies for Team Environments

For team and production deployments, add a server-side policy layer. Create a bundle configuration for the KeyZero PDP server:

version: "1"

issuers:
  - name: github-actions
    type: github-actions
    issuer_url: "https://token.actions.githubusercontent.com"
    audience: "keyzero"

policies:
  - name: allow-deploy-prod
    rule: "org == 'myorg' && action == 'deploy.yml' && ref.matches('secret/data/prod/**')"
    effect: allow
  - name: allow-dev
    rule: "ref.matches('secret/data/dev/**')"
    effect: allow
  - name: default-deny
    rule: "true"
    effect: deny

resources:
  - ref: "secret/data/prod/**"
    resolver: vault-prod
    mode: direct
  - ref: "secret/data/dev/**"
    resolver: local
    mode: direct

backends:
  vault-prod:
    type: hashicorp_vault
    address: https://vault.example.com
  local:
    type: env_file
    path: ./secrets.env

Start the server with kz server start --bundle ./bundle.yaml. Every resolve request now requires a valid JWT and must pass CEL policy evaluation before any secret is returned.

Common Pitfalls and How to Avoid Them

Pitfall: Migrating all secrets at once. Start with non-critical development secrets. Validate the workflow before touching production credentials. Roll out provider by provider.

Pitfall: Forgetting to remove old secrets. After migrating a secret to a vault, delete it from .env files, CI variables, and any other location found during the audit. Leaving stale copies defeats the purpose.

Pitfall: Not using .keyzero.local.toml for personal overrides. Developers who override shared config directly in .keyzero.toml cause merge conflicts and accidental credential exposure. Use the local override file.

Pitfall: Skipping blind mode for AI agents. Direct mode is simpler, but any secret resolved in direct mode is visible to the subprocess. If the subprocess is an AI agent, blind mode is the correct default.

Pitfall: Overly broad policies. A policy like rule: "true" with effect: allow defeats the purpose of policy-based access control. Scope every allow rule to specific identities and secret paths.

Pitfall: Not validating the bundle. Run kz server check --bundle ./bundle.yaml before deploying. This catches CEL syntax errors, missing backend references, and configuration issues before they cause runtime failures.