Skip to content

Security: aminueza/Keyfence

SECURITY.md

Security

Reporting a vulnerability

Use GitHub's private vulnerability reporting on this repository (Security tab, "Report a vulnerability"). Do not open a public issue for a security problem. You will get an acknowledgement within a few days and a fix or a documented decision before any public disclosure.

What keyfence sees and stores

keyfence is a man-in-the-middle proxy for the hosts it monitors. It reads the full body of every request to those hosts, every text WebSocket frame on a connection to them and, in placeholder mode, the full response. This is the point of the tool, and it is also why it runs only on your machine, listens only on 127.0.0.1 by default, and never sends anything anywhere except to the provider you were already talking to.

On disk it keeps:

  • ~/.keyfence/vault.json: keyed HMAC-SHA256 digests of your secrets, never the values, mode 0600. The key is a random 256-bit salt kept in the same file, so anyone who can read the file can test guesses against the digests offline, and a short or common password does not survive that. A slow password hash such as PBKDF2 or scrypt is not an option here: the proxy computes one digest per token of every request, a few hundred per request, and must answer in milliseconds. Protect the file like the CA key below, and prefer registering long random values.
  • ~/.keyfence/audit.log: one line per request with findings, with a masked preview and the JSON key, never the value. A preview is two characters at each end of a value of 24 characters or more; a shorter value is logged as its kind and its length alone, because a preview of a short secret is most of the secret. Mode 0600, like the vault.
  • ~/.keyfence/env/: the environment snapshot of a running keyfence exec, hashes only, deleted by the proxy at startup.
  • ~/.keyfence/proxy.log: the proxy's own output during keyfence exec: the startup summary (version, mode, host count, rule count, vault size) and one line per detection with the host, the number of secrets and their kinds. The CANARY tripped line names the file the canary was planted in. Never a value. Mode 0600, like the vault.
  • the file given to keyfence exec --record FILE, only when you ask for it: a mitmproxy flows file with every request that reached the proxy, headers and body in full, in the state keyfence left it. That includes the Authorization header with the provider's own key in every mode. Responses are streamed through and not kept, except the ones keyfence produced or rewrote: the 403 of a blocked request and, in placeholder mode, a response with the real values put back. In redact and placeholder mode the requests carry [REDACTED:...] or placeholders instead of the secrets. In audit mode nothing is changed, so the file holds your secrets in clear text; in block mode it holds the blocked request as the command sent it, secrets included. keyfence exec says so on stderr in those two modes. The file is created with mode 0600. Treat it as the transcript it is and delete it when you are done.
  • ~/.mitmproxy/: the CA certificate and key generated by mitmproxy. Anyone who can read that key can intercept TLS on this machine for any client that trusts the CA. Protect it like an SSH key.

The notice feature appends a short text to the system prompt of requests it changed. That is the only content keyfence adds to a request.

Threat model

In scope: accidental leakage of secrets through prompts, tool results and files that coding agents send to LLM providers. keyfence is a last line of defence and works best with the Claude Code hook that stops the files from being read at all.

Out of scope: a program that deliberately hides secrets (encoding, splitting, encryption), certificate-pinning clients, traffic that bypasses the proxy, and secrets in headers or query strings, which are left alone because they usually carry the provider's own key. See docs/limitations.md.

Supply chain

  • Releases are built and published by the Release GitHub Actions workflow through PyPI trusted publishing. No long-lived token exists. PyPI records a provenance attestation for every file, linking it to the workflow run and commit; it is shown on the PyPI file listing and can be checked with pypi-attestations.
  • CI builds a CycloneDX SBOM for every commit on main and every pull request; it is attached to the workflow run as an artifact.
  • Dependencies are mitmproxy and PyYAML, both from PyPI. The bundled detection rules come from the gitleaks repository and are checked in, so nothing is fetched at runtime.
  • Tests run on Linux, macOS and Windows.

Design choices that limit blast radius

  • A detector exception fails closed: the request gets a 403, and a WebSocket frame is dropped instead of forwarded.
  • A corrupted vault stops the proxy at startup with a message and makes a running proxy fail closed until it is fixed.
  • An empty or missing config file does not change a running proxy.
  • keyfence run replaces its own process with mitmproxy, so killing it cannot leave a proxy behind.

There aren't any published security advisories