K0KEYZERO
← All posts

How KeyZero's Architecture Keeps Secrets Off Disk

How KeyZero keeps secrets off disk: the blind-mode MITM proxy, CEL policy decision point, multi-backend provider abstraction, and shell hooks

·architecture, deep-dive, technical

The Resolution Pipeline

KeyZero has one job: resolve secrets at runtime and deliver them to processes without writing credentials to disk. The architecture is built around four components that compose into two deployment modes (solo and team). Every component is part of a single Rust binary (kz) distributed via npm, Homebrew, or Cargo.

The flow is: configuration defines what secrets are needed, a provider fetches them from a vault backend, and a delivery mechanism gets them to the consuming process. Blind mode adds a proxy layer that prevents the process from ever seeing raw values.

Component 1: Shell Hooks

Shell hooks make secret resolution automatic. Instead of running kz run manually, you install a hook that triggers on directory change:

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

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

# fish
kz hook --shell fish | source

When you cd into a directory containing .keyzero.toml, the hook calls kz run implicitly and loads the resolved secrets into the current shell session. When you cd out, the secrets are unloaded.

How It Works Internally

The hook registers a chpwd function (zsh), PROMPT_COMMAND (bash), or --on-variable PWD event (fish). On each directory change, it walks up from the current directory looking for .keyzero.toml. If found, it resolves the configured secrets and exports them. If not found, it cleans up any previously exported variables.

This is the developer experience layer. It ensures that DATABASE_URL is always available when you are in the project directory and never leaks into unrelated work.

Component 2: Provider Abstraction

KeyZero uses a unified provider interface to resolve secrets from any backend. The .keyzero.toml config maps environment variable names to provider-specific references:

[secrets]
DATABASE_URL = { provider = "keychain", ref = "myapp-db-url" }
API_KEY      = { provider = "1password", ref = "op://Engineering/myapp/api-key" }
AWS_SECRET   = { provider = "aws", ref = "sm://prod/aws-secret" }

Supported Backends

ProviderBacking StoreAuth Mechanism
keychainmacOS KeychainSystem authentication
onepassword_cli1Password via op CLIOP_SERVICE_ACCOUNT_TOKEN or op signin
hashicorp_vaultHashiCorp Vault KV v2VAULT_TOKEN
aws_secrets_managerAWS Secrets ManagerAWS SDK credential chain
aws_stsAWS STS AssumeRoleAWS SDK credential chain
env_fileLocal KEY=VALUE fileFilesystem access

Each provider implements the same interface: given a reference string, return the secret value. The provider handles authentication, connection management, and response parsing internally.

Environment-Specific Resolution

The .keyzero.toml file supports environment sections that override the default:

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

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

Setting KEYZERO_ENV=production (or NODE_ENV, RAILS_ENV) activates the production section. This means the same config file works across local development, CI, and production with different backend providers.

Component 3: The MITM Proxy (Blind Mode)

Blind mode is KeyZero's network-level protection for untrusted workloads — see blind mode explained for a focused walkthrough. When you run kz run --blind, the following happens:

  1. Secret resolution. KeyZero resolves all configured secrets from their providers.
  2. Token masking. Each secret value is replaced with an opaque token (kz_masked_<hash>). The real values are stored in a local lookup table.
  3. Proxy startup. A local MITM proxy starts on a random port. An ephemeral CA certificate is generated for TLS interception.
  4. Environment injection. The subprocess receives the masked tokens as environment variables, plus HTTP_PROXY, HTTPS_PROXY, SSL_CERT_FILE, NODE_EXTRA_CA_CERTS, and REQUESTS_CA_BUNDLE pointing to the proxy and CA cert.
  5. Request interception. When the subprocess makes an HTTP/HTTPS request, the proxy intercepts it, scans for masked tokens in headers and body, replaces them with real values, and forwards the request to the upstream server.
  6. Response passthrough. The upstream response is returned to the subprocess unmodified.

The subprocess sees kz_masked_7f3a9b... in its environment. The upstream API receives sk-proj-abc123.... The real credential exists only in the proxy's memory, never on disk, never in the subprocess's address space.

Connection Control

Blind mode includes host-level allowlisting to prevent credential exfiltration:

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

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

This restricts the subprocess to only the declared hosts. A compromised agent cannot send credentials to an attacker-controlled endpoint because the proxy blocks the connection.

Component 4: Policy Decision Point (PDP)

The PDP is the team/production mode component. It runs as kz server start --bundle ./bundle.yaml and evaluates every resolve request through three stages:

JWT Verification

The caller presents a JWT. The PDP validates signature, issuer, audience, and expiration against configured issuers. Built-in profiles for GitHub Actions, Kubernetes, GCP, and AWS automatically normalize JWT claims into standard identity variables.

CEL Policy Evaluation

Policies are CEL expressions evaluated top-down, first-match-wins. For detailed examples and design guidelines, see policy-based access control for machine identities. The expression context includes verified identity fields (org, service, env, action, branch, actor, groups), the requested secret path (ref), raw JWT claims (claims.*), and untrusted caller context (context.*).

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

Resource Resolution

If policy allows, the PDP resolves the secret from the matching backend. Resources use ref patterns with glob matching, and the most specific match wins.

How Components Compose

Solo Mode

Solo mode is a single developer or CI runner using the kz CLI directly. The active components are:

  • Shell hooks (optional) for automatic loading on cd
  • Provider abstraction for resolving from vaults
  • MITM proxy (optional, via --blind) for AI agent workloads

No server process is required. Configuration lives in .keyzero.toml. This is the default starting point.

Team Mode

Team mode adds the PDP server for centralized policy enforcement. The active components are:

  • PDP server running kz server start --bundle ./bundle.yaml
  • JWT verification for caller authentication
  • CEL policies for authorization
  • Provider abstraction for backend resolution
  • Credential-swapping proxy (optional, via --proxy) for short-lived token mode

In team mode, clients authenticate with JWTs and the server enforces policies before resolving secrets. The server can also run in MCP mode (--mcp) to serve AI agents directly via the Model Context Protocol. Team mode supports containerized deployments on Docker and Kubernetes.

Performance Characteristics

Cold start. The kz binary is a native Rust executable. CLI startup is under 10ms. Server startup includes loading the bundle and initializing backend connections, typically under 100ms.

Resolution latency. Secret resolution latency is dominated by the backend. Local providers (Keychain, env_file) resolve in under 5ms. Remote providers (Vault, AWS Secrets Manager, 1Password CLI) add network round-trip time, typically 50-200ms depending on the backend and network conditions.

MITM proxy overhead. The blind mode proxy adds approximately 1-3ms per request for token scanning and replacement. TLS handshake overhead is amortized across connections to the same host.

Caching. The CLI resolves secrets once at startup and injects them into the subprocess environment. The server supports TTL-based caching via the ttl field on resources. Short-lived mode tokens have configurable expiration (default 300 seconds).

Bundle validation. kz server check --bundle ./bundle.yaml validates the entire configuration (CEL syntax, backend references, issuer configuration) without starting the server. Use this in CI to catch configuration errors early.