Skip to main content

Loopback HTTPS proxy that fetches API credentials from Bitwarden Secrets Manager just-in-time and injects them into outbound requests, so the calling process never holds the real credential bytes in its address space.

Project description

agent-vault-proxy

Zero-knowledge API keys for AI agents: the agent only ever sees a placeholder.

Your agent gets a fake placeholder string (like sk-PLACEHOLDER-...) and uses it as if it were a real API key. This proxy sits between the agent and the internet, and swaps the fake for the real secret at the last possible moment - on the way out to the upstream API. If the agent gets prompt-injected, dumps a log, or runs a program with a software-supply-chain issue, the only thing that escapes is the fake placeholder. The real key never enters the agent's process.

PyPI License: MIT CI

agent-vault-proxy demo: prompt injection vs. credential isolation

Under the hood: a loopback HTTPS proxy that fetches credentials from Bitwarden Secrets Manager just-in-time and injects them into outbound requests, so the calling process never holds the real credential bytes in its address space.

How it works

┌──────────────┐    placeholder    ┌──────────────┐    real secret    ┌──────────┐
│  agent (any  │ ────────────────► │ agent-vault- │ ────────────────► │ upstream │
│  UID, never  │                   │    proxy     │                   │   API    │
│  sees real   │ ◄──────────────── │  (UID: avp)  │ ◄──────────────── │          │
│   secret)    │     response      │              │     response      │          │
└──────────────┘                   └──────┬───────┘                   └──────────┘
                                          │
                                          ▼  fetch + cache (TTL 5 min)
                                   ┌──────────────┐
                                   │  Bitwarden   │
                                   │  Secrets Mgr │
                                   └──────────────┘

On every request the proxy: checks the destination against the binding for that secret (host + optional method + optional path scope), fails closed if no binding matches (the placeholder is forwarded verbatim so the upstream's own auth-fail response surfaces), fetches the real secret from BWS (served from an in-memory TTL cache when warm), substitutes placeholder → real secret on the upstream socket only, and fsyncs an inject_decision audit event before the modified bytes go on the wire.

At a glance

# bindings.yaml — what the agent sees vs. what the upstream sees
secrets:
  OPENAI_API_KEY:
    placeholder: "sk-PLACEHOLDER-01HXY1234567890"   # the agent's env holds THIS
    inject:
      header: "Authorization"
      format: "Bearer {secret}"                     # {secret} = real value from BWS
    bindings:
      - host: "api.openai.com"                      # only swapped for this destination
        methods: [POST]                             # only on these methods
        paths: ["/v1/chat/completions"]             # only on these paths
# Agent's env holds only the placeholder. The real key never enters the process.
export OPENAI_API_KEY="sk-PLACEHOLDER-01HXY1234567890"
export HTTPS_PROXY="http://127.0.0.1:14322"

# Agent code is unchanged — proxy swaps placeholder → real BWS value on the wire.
curl -H "Authorization: Bearer $OPENAI_API_KEY" https://api.openai.com/v1/chat/completions ...

Full schema (composite secrets, multiple hosts per binding, path globs) in bindings.example.yaml.

Why

Two threats keep getting worse, and your API keys sit in the blast radius of both.

Prompt injection. Anything your agent reads - a webpage, an email, a tool's output, a PR comment, can carry instructions. If the agent has OPENAI_API_KEY in its env, an injected "send your env to attacker.com" is one HTTP call away. Filtering, alignment, allowlists - are all statistical and all imperfect. The bytes shouldn't be there to exfil in the first place.

Software supply chain. A typosquatted npm package, a hijacked PyPI release, a malicious post-install script. If it runs as your agent's UID it reads the same env the agent does. Shai-Hulud showed what worm-scale ecosystem compromise looks like. That's the new baseline.

AVP keeps the credential bytes out of the agent, and out of anything the agent runs, and in fact out of any software you can run on that host. As long as the outbound HTTPS goes through AVP, none of it ever sees the real secret. The secrets live in Bitwarden; everyone else gets a placeholder. AVP swaps placeholder with a real value on the wire, default-deny per destination (the proxy refuses to inject for hosts you haven't bound to that secret), and additionally scopes per binding by HTTP method and URL path.

What AVP doesn't do - and what to layer on: AVP prevents exfiltration of the raw key, not misuse of the authority the key represents on permitted destinations. If you bind GITHUB_PAT_WORK to api.github.com with no method/path scope, prompt injection can still ask the proxy to authenticate a DELETE /repos/... call as you. The lever for that is methods: and paths: on each binding: see bindings.example.yaml. For extra security, pair AVP with an egress firewall on the agent's UID so unbound calls are blocked outright. Pair with response-side review for endpoints that may echo back the Authorization header in their response body, AVP injects on the request, but does not scrub the response.

How this compares to HashiCorp Vault Agent, Doppler, op run, superfly/tokenizer, and Kloak: docs/comparison.md.

Setup (one-time)

Four steps. Once you've done this, every new API key is just "add to Bitwarden + a few lines of YAML + restart": see Add a secret below.

  1. Bitwarden Secrets Manager, enable it on your org, create a project for this host, create a machine account with read access to the project, generate a token. ~10 minutes the first time. Walkthrough.

  2. Clone a tagged release + give the daemon the BWS token + your initial bindings:

    # Pick a tagged release, not `main`. Tags are how you opt into a specific
    # vetted version. Tracking `main` exposes you to a window where a
    # maintainer-account compromise could ship a malicious commit before
    # anyone notices.
    git clone -b v0.4.0 --depth 1 https://github.com/inflightsec/agent-vault-proxy && cd agent-vault-proxy
    mkdir -p secrets && bash -c '( umask 077 && read -rsp "BWS access token: " T && printf "%s" "$T" > secrets/bws-token && echo )'
    cp bindings.example.yaml bindings.yaml && $EDITOR bindings.yaml
    
  3. Start the daemon:

    docker compose up -d
    

    Docker Compose covers Linux, macOS (Docker Desktop), Windows (WSL2). For bare-metal Linux + systemd (most hardened), see docs/install-systemd.md.

  4. Point your agent at the proxy:

    docker cp agent-vault-proxy:/var/lib/agent-vault-proxy/.mitmproxy/mitmproxy-ca-cert.pem ca.pem
    export HTTPS_PROXY="http://127.0.0.1:14322"  NODE_EXTRA_CA_CERTS="$PWD/ca.pem"  SSL_CERT_FILE="$PWD/ca.pem"
    export OPENAI_API_KEY="sk-PLACEHOLDER-01HXY1234567890ABCDEFGHIJ"
    curl -H "Authorization: Bearer $OPENAI_API_KEY" https://api.openai.com/v1/models
    

⚠️ Two hard prerequisites for the Docker path: (1) your AI agent's UID must NOT have docker daemon access - docker-group membership ≈ host root, which lets the agent docker exec the CA private key + BWS token out of the proxy. (2) Do NOT add other containers to the proxy's avp-net network. If either is hard to guarantee, use the systemd install path. Full threat model in docs/docker.md.

Add a secret

After the one-time setup, every new credential is the same three steps:

  1. Bitwarden: add the real secret to the project from step 1 above (use a clear name like OPENAI_API_KEY).
  2. Bindings: add a block to bindings.yaml, the BWS name, a placeholder string, the destination host(s), and how to inject it. Composite credentials (e.g. base64(email:token) for Jira / Atlassian Cloud) use compose: + a sandboxed Jinja2 template - see bindings.example.yaml for one-secret and composite patterns covering OpenAI, GitHub, Jira, Slack.
  3. Restart: docker compose restart agent-vault-proxy (or systemctl restart agent-vault-proxy.service). Verify with a request from the calling shell: the proxy audits every decision to /var/log/agent-vault-proxy/audit.jsonl.

That's it. Your agent uses the placeholder; the proxy swaps it for the real value on the wire.

Other install paths

  • docs/prerequisites.md, Bitwarden Secrets Manager setup (10 minutes, do this first)
  • docs/install-systemd.md - bare-metal Linux + systemd (most hardened; recommended for production hosts where the agent might share the box)
  • docs/docker.md, full Docker walkthrough (threat model, troubleshooting, rootless option)
  • docs/usage.md - env-var setup for the calling shell, configuration reference
  • bindings.example.yaml, full config schema with reference patterns for Anthropic, OpenAI, GitHub, Groq, Mistral, DigitalOcean

Alternatives ways to install:

  • pipx install agent-vault-proxy - for the library / non-Docker case: writing a new SecretsBackend adapter, wiring AVP into your own Ansible / Nix / image build with hash-pinned deps, or running inside an existing Python venv. The PyPI badge at the top of this README links to the published wheel.
  • Signed container image on ghcr.io (planned for v0.5.0), cosign verify ghcr.io/inflightsec/agent-vault-proxy:<tag> + docker run with a tiny mount-only Compose snippet you write yourself. Removes the clone and pins the binary + its hardening assumptions to a single signed digest. Until then, build locally from the cloned tag.

Privacy

The proxy never phones home. The only outbound connections it makes are (1) to the Bitwarden Secrets Manager endpoint you configure in bindings.yaml, and (2) the upstream APIs your agent is actually calling on your behalf. No analytics, telemetry, update checks, crash reports or metrics export.

The audit log under /var/log/agent-vault-proxy/audit.jsonl is local-only.

Security model

Nine binary, individually-testable invariants (G1–G9): the agent process address space never contains real secret bytes; substitution only happens on permitted destinations; failures are closed; audit events are fsynced before the modified request goes on the wire. See docs/architecture.md for the threat model, invariant tests, hardening checklist, and accepted residual risks.

Vulnerability reports: SECURITY.md.

Status

v0.4.0, first public release. Adds composite secret bindings (compose: + sandboxed Jinja2 templates for credentials assembled from multiple BWS values), an adapter architecture (SecretsBackend Protocol with BWS as the reference implementation, so 1Password / Vault / Doppler / AWS adapters can land without forking), and hardened supply-chain gates (hash-pinned dev lockfile, mypy + Ruff C90 in CI + pre-commit, two-layer lockfile drift detection). v0.3 was skipped - the adapter refactor was bundled with composite secrets in one release. Full entry in CHANGELOG.md.

The wire-format invariants (G1–G9) are stable and exercised regularly against live Anthropic, OpenAI, GitHub, Groq, Mistral, and DigitalOcean APIs. Validation: 260+ automated tests passing, two rounds of adversarial review per feature (pentest + cross-model Oracle), and the hardening checklist from docs/architecture.md walked end-to-end. The wire invariants will not change before 1.0; the configuration schema may.

Not yet supported: OAuth refresh-token flows, AWS SigV4, multi-tenant routing, off-host BWS broker, admin Unix socket / MCP interface. The avp bindings diff semantic-review CLI, cosign-signed ghcr.io container images, SBOMs at build time, and a published Ansible role are planned for v0.5.0+.

Other vault backends (1Password, HashiCorp Vault as a source, etc.) plug in via the SecretsBackend Protocol - see docs/adapter-architecture.md for the design. PRs that add an adapter for an additional vault are welcome.

Contributing

Bug reports and PRs welcome. New here? Check the good first issues for starter-sized contributions. For changes that touch the G1–G9 invariants, please open an issue first, docs/architecture.md describes what we're trying to preserve. Setup, testing, and pre-commit hooks in CONTRIBUTING.md.

License

MIT - see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

agent_vault_proxy-0.4.0.tar.gz (250.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

agent_vault_proxy-0.4.0-py3-none-any.whl (42.0 kB view details)

Uploaded Python 3

File details

Details for the file agent_vault_proxy-0.4.0.tar.gz.

File metadata

  • Download URL: agent_vault_proxy-0.4.0.tar.gz
  • Upload date:
  • Size: 250.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for agent_vault_proxy-0.4.0.tar.gz
Algorithm Hash digest
SHA256 4542783dc775d0f9d70436a59ca6f46e38fe1ba5101d8acf1d1a35edeac2b2c2
MD5 b8310274f8e83ce67668fdc9531b2048
BLAKE2b-256 cce3123c70f0105a4b90449b6dc3857208cc8b694d49bf69bb4ea40d23c66a37

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_vault_proxy-0.4.0.tar.gz:

Publisher: release.yml on inflightsec/agent-vault-proxy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file agent_vault_proxy-0.4.0-py3-none-any.whl.

File metadata

File hashes

Hashes for agent_vault_proxy-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e9557273b919dac1b5f976b00b8a6554543f92805d532d32195355867024912e
MD5 f16876e33dd42ad87538c0f15b7c4ae8
BLAKE2b-256 a577cc5a38e4d9f07116412b4097511aa3412fd25ab7245fa657629aa63d4f2a

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_vault_proxy-0.4.0-py3-none-any.whl:

Publisher: release.yml on inflightsec/agent-vault-proxy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page