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
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:
.envfiles -- 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 --
exportstatements,curlcommands 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:
| Backend | Best For | TOML Provider / Bundle Type |
|---|---|---|
| macOS Keychain | Local development | keychain |
| 1Password CLI | Team workflows | 1password / onepassword_cli |
| HashiCorp Vault | Production infrastructure | hashicorp_vault |
| AWS Secrets Manager | AWS-native workloads | aws_secrets_manager |
| Environment file | Quick testing | env_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.