Security fixes land on the latest release track (v2.x). Older releases are not maintained.
Please do not open a public issue for a security problem.
Use GitHub's private vulnerability reporting: open the Security tab of this repository and choose Report a vulnerability. That channel is visible only to the maintainers.
A useful report includes:
- the affected version, tag or commit;
- what the issue is and which component it affects (proxy, monitor, MCP, storage, recording format);
- reproduction steps or a minimal config;
- the impact as you understand it, and any suggested fix.
We aim to acknowledge a report within a few days, agree on a disclosure timeline with you, and credit you in the release notes unless you prefer otherwise.
Trajecta is local-first, but it handles real credentials and real conversation content, so a deployment still needs care:
- Cassettes store raw traffic. With the default
debug.mask_key: truethe recorder replaces theAuthorizationvalue withBearer fake-key-loggingand theapi-key/x-api-key/x-goog-api-keyvalues withfake-key-loggingin the recorded request headers before writing. Every other header, the request body, and the whole response are stored verbatim — that is what makes replay faithful. If you setdebug.mask_key: false, credentials are written to disk in plaintext. Either way, treattrace.output_dir/TRAJECTA_OUTPUT_DIRas sensitive and do not commit recordings of production traffic. - Bodies are verbatim unless secret masking is on.
debug.mask_keyrewrites four request headers in the cassette; it does not touch bodies.secret_masking.enabled(on by default, and turned off with an explicitfalse) goes further: a credential found in a request body is replaced by a same-length, format-preserving stand-in before the request reaches the provider, the stand-in is restored to the real value in the response, and the cassette still records the original body so replay and human inspection keep working. That last point is deliberate and it is the operator's risk to weigh: with masking on, the cassette on disk still contains the real credentials, exactly as it did before. What changes is what leaves the machine. The mapping from stand-in back to value exists only in the process that rendered it, is seeded fromcrypto/randat startup, and is never written to the cassette, so a stand-in copied out of a trace cannot be reversed without that process. The rewrite itself is recorded twice: asecret.maskedprelude event in the cassette carries the rule and the offsets but no bytes, and onecredential_mask_auditsrow carries the real value and the stand-in that replaced it. That table is the one place the pair is stored, and it is the operator's risk to weigh alongside the cassette - the values are sealed with the same local key as the channel credentials when that key exists, and stored in the clear when it does not, with each row'sstorage_moderecording which happened. It is readable throughGET /api/secret-masksand the MCP toollist_credential_masks, so treat it like the cassette directory rather than like a log, and read docs/SECRET_MASKING.md before relying on it: it lists what the engine covers and, more importantly, what it does not - private-key bodies, encoded or truncated forms, and operations that need the real value to work are all outside it. - URLs and DSNs are redacted before they are stored or printed. A URL that reaches a cassette, the
logstable, a log line orconfig inspecthas the password in its userinfo replaced, every query parameter whose name matches the shared marker list (key,token,secret,password,passwd,credential,signature,sig,access_token,api_key,authorization,auth) replaced with a placeholder, and a userinfo username replaced when it matches that same list. Database DSNs are redacted in both shapes: the URL form covers the userinfo and the query string, and the keyword form covers everykey=valuefield. The residual is a userinfo username that names no secret, such as the literal API key inhttps://sk-live-abc@gateway.example.com/v1: the username is kept because it identifies the upstream, so do not configure an upstream that way — put the credential in the channel API key, which is encrypted.debug.mask_key: falsedoes not disable this redaction. - Channel credentials are encrypted at rest. API keys and secret headers are sealed with AES-256-GCM under a random 32-byte local key stored at
<output_dir>/trace_index.secret(mode0600). Back that file up together with the application database: without it the stored channel credentials cannot be decrypted. - Change the default Compose credentials.
POSTGRES_PASSWORDin.env.exampleis an example value; the trackedconfig/config.yamlintentionally contains no provider keys. - Do not expose the monitor port to untrusted networks. The monitor, the proxy API and the MCP endpoint all authenticate (login session or a personal
Bearertoken), and the monitor is where provider credentials and recorded traffic are readable. Keep it on localhost or behind your own access control. - Personal tokens are hashed, not recoverable. Treat a leaked token as compromised and revoke it in the monitor; it cannot be looked up from the database.