# Trust center

Vaubaan Gateway is pre-release software that holds your cloud credentials so that coding agents never do; this page says what protects them, what does not, and what leaves an instance, each claim linked to the document or the code that backs it.

**Status, plainly.** No version is tagged and no image is published yet; the software has not been run in production ([README](https://github.com/vaubaan/gateway/blob/main/README.md#status), [known limitations](/docs/known-limitations)). There has been no third-party security audit and there is no certification. The threat model and the controls below are the project's own work, reviewed in its repository.

## The trust proposition

- **The agent never holds a provider credential.** It holds a mission key. The gateway decrypts the stored credential in memory for the duration of one call, calls the service, and returns only the result ([threat model](https://github.com/vaubaan/gateway/blob/main/docs/handbook/threat-model.md#the-trust-proposition), [SECURITY.md](https://github.com/vaubaan/gateway/blob/main/SECURITY.md#trust-model)).
- **One named exception.** A few tools exist to hand the agent a short-lived token, such as a GitHub installation token to clone or push. Each is a permission of its own, refused unless the mission holds it; what the agent then does with the token happens outside the gateway (accepted risk A21 of the [threat model](https://github.com/vaubaan/gateway/blob/main/docs/handbook/threat-model.md#accepted-risks)).
- **Three credentials that never cross.** A mission key opens the execution door (`/mcp`) only; a delegated token opens the management door (`/mcp/manage`) only; a browser session opens the web interface only, and only a browser session answers a request or an approval. Each door refuses the other two by kind, so an agent can neither widen its own mission nor approve its own action ([threat model](https://github.com/vaubaan/gateway/blob/main/docs/handbook/threat-model.md#the-three-credentials-and-why-none-crosses); proof: [credential-separation.test.ts](https://github.com/vaubaan/gateway/blob/main/apps/gateway/tests/integration/oauth-as/credential-separation.test.ts)).
- **One instance, one company.** The instance runs on your network, behind your identity provider, with apps your company registered at each service. Whoever operates it holds the master key and can read a decrypted credential in the memory of a running call: the gateway does not defend against its own operator, and makes the operator's acts visible instead ([threat model](https://github.com/vaubaan/gateway/blob/main/docs/handbook/threat-model.md#the-trust-proposition), [ADR](https://github.com/vaubaan/gateway/blob/main/docs/decisions/trust-model-clear-at-rest-idp-style.md)).

## Controls

- **Encryption at rest.** Each stored credential is sealed with XChaCha20-Poly1305 under a key derived per row from the master key, the row id as associated data; the master key lives in the configuration, never in the database, so a copy of the database alone opens nothing ([threat model, assets](https://github.com/vaubaan/gateway/blob/main/docs/handbook/threat-model.md#assets), [crypto module](https://github.com/vaubaan/gateway/blob/main/apps/gateway/src/server/crypto/aead.ts)).
- **Master key rotation reseals every credential.** A new key version, then a command that re-encrypts every stored secret under it, with a dry run first ([rotation procedure](/docs/operations/master-key-rotation), [ADR](https://github.com/vaubaan/gateway/blob/main/docs/decisions/master-key-rotation-reseals-every-blob.md), [reseal code](https://github.com/vaubaan/gateway/tree/main/apps/gateway/src/server/reseal)).
- **Keys stored as digests.** A mission key is shown once to its holder; the store keeps a SHA-256 digest of its 32-character random secret ([threat model, assets](https://github.com/vaubaan/gateway/blob/main/docs/handbook/threat-model.md#assets)).
- **Every call decided, fail closed.** The mission's resources first, then every slice of the call must match a permission the mission accepted; an unknown tool, an uncovered slice or a stage that throws refuses the call ([threat model, missions](https://github.com/vaubaan/gateway/blob/main/docs/handbook/threat-model.md#missions-and-the-execution-door)).
- **Limits and approvals.** A limit is decided and counted in one transaction, so two calls cannot spend the last credit twice; an approval is bound to the mission, the permission, the tool and a fingerprint of the arguments, and is used once ([approval tests](https://github.com/vaubaan/gateway/blob/main/apps/gateway/tests/integration/requests/approval.test.ts)).
- **The instance's own hosting is never a mission resource.** The Railway project or Fly app the gateway runs in, and any resource listed in `self_resources`, is refused to every mission ([ADR](https://github.com/vaubaan/gateway/blob/main/docs/decisions/self-resources-never-reachable-by-a-mission.md), [tests](https://github.com/vaubaan/gateway/blob/main/apps/gateway/tests/integration/execution/self-resources.test.ts)).
- **Sign-in hardening.** OpenID Connect with PKCE, single-use state and nonce; an unverified email is refused; per-address and instance-wide rate limits on sign-in; host-only `__Host-` session cookies that end after one hour without a request and eight hours at most; every mutation origin-checked ([threat model, sign-in](https://github.com/vaubaan/gateway/blob/main/docs/handbook/threat-model.md#sign-in-and-the-web-session)).
- **Security headers set by the application** on every response, whatever proxy is in front: a nonce-based Content-Security-Policy, framing refused, `nosniff`, a referrer policy, no device permission, HSTS for an https public URL ([ADR](https://github.com/vaubaan/gateway/blob/main/docs/decisions/security-headers-set-by-the-application.md), [code](https://github.com/vaubaan/gateway/blob/main/apps/gateway/src/server/http/security-headers.ts)).
- **Every call recorded.** Each call writes one audit entry, allowed or refused, with the permissions it needed and whose key made it; arguments are kept as a fingerprint only; the application cannot edit an entry, and only the retention purge deletes old ones ([audit handbook](https://github.com/vaubaan/gateway/blob/main/docs/handbook/audit.md), [threat model, audit](https://github.com/vaubaan/gateway/blob/main/docs/handbook/threat-model.md#audit-logs-telemetry)).

## Supply chain

- **Dependency audit gate.** `npm audit` gates every change and every release against a reviewed, dated allowlist ([audit-gate.mjs](https://github.com/vaubaan/gateway/blob/main/scripts/audit-gate.mjs)).
- **Lockfile gate.** Every installed package comes from the npm registry, over https, with a sha512 integrity hash, under the name it claims ([lockfile-gate.mjs](https://github.com/vaubaan/gateway/blob/main/scripts/lockfile-gate.mjs)).
- **Security checks in CI.** Secret scanning of the whole history (gitleaks), static analysis (Semgrep with the project's own rules), an image scan (Trivy) that fails on a known vulnerability with a fix; every tool pinned by checksum or digest ([coding workflow](https://github.com/vaubaan/gateway/blob/main/docs/workflow/coding-workflow.md#security-checks), [ADR](https://github.com/vaubaan/gateway/blob/main/docs/decisions/supply-chain-and-ci-security-checks.md)).
- **The image is built from source**, on base images pinned by digest, runs as a non-root user and ships no package manager ([Dockerfile](https://github.com/vaubaan/gateway/blob/main/deploy/docker/Dockerfile), [deploy README](https://github.com/vaubaan/gateway/blob/main/deploy/README.md#what-the-image-contains)).
- **Planned: signed images and provenance.** The release pipeline that publishes multi-architecture images with an SBOM and a signed build-provenance attestation is written but has never run; until the first release, you build the image yourself ([release workflow](https://github.com/vaubaan/gateway/blob/main/.github/workflows/release.yml), [how to verify, once releases exist](https://github.com/vaubaan/gateway/blob/main/deploy/README.md#released-images)).

## What leaves an instance

**Nothing, by default.** Community telemetry is off on a new instance (`telemetry.enabled`, default `false`). And today nothing leaves even when it is turned on: the built-in collector address is still gated as a placeholder, and the gateway sends nothing to it until a release publishes it ([telemetry handbook](https://github.com/vaubaan/gateway/blob/main/docs/handbook/telemetry.md), [known limitations](/docs/known-limitations)). An administrator who turns telemetry on sees the exact body of a report on Settings → Administration → Telemetry before anything is sent.

When an operator turns it on and points the instance at a collector, the instance sends one of two levels (`telemetry.level`; the default level of an enabled instance is `usage-stats`):

**Level `plugin-health`** — one report per call a plugin executed, exactly seven fields:

| Field | What it holds |
| --- | --- |
| `plugin` | the plugin's id, such as `github` |
| `plugin_version` | the plugin's version |
| `gateway_version` | the gateway's version |
| `tool` | the tool's name, only if the plugin declares it |
| `outcome` | `success`, `upstream-error`, `classification-error` or `execution-error` |
| `error_class` | null for a success, else a fixed keyword or `http_<status>`; never a message |
| `duration_bucket` | `lt_100ms`, `lt_1s`, `lt_10s` or `gte_10s` |

**Level `usage-stats`** adds:

- one **decision** per call the gateway decided: the plugin, the tool (null when the agent named a tool the plugin does not declare), the gateway version, `allowed` or the refusal code, the permissions touched without the environment label your company chose (`github.git-push`, never `github[prod].git-push`), and whether an approval was involved;
- one **instance profile** per UTC day: the gateway version, the kind of deployment (`docker`, `railway`, `fly` or `other`), the runtime and architecture, the kinds of sign-in in use, and counts as buckets only (`0`, `1`, `2-5`, `6-20`, `21-100`, `101-1000`, `>1000`) of users, running missions, calls and accounts per plugin; a day identifier of 128 random bits drawn again every UTC day and kept in memory only, never attached to another report.

**Never sent, and impossible for the collector to store:** the arguments of a call or a hash of them, payloads, provider answers or error text, resource references, environment labels, mission, user or connection identifiers, emails, account names, host names, URLs, a stable identifier of the instance, a network address. No report carries a time; the collector stores the UTC hour a row arrived ([collector README](https://github.com/vaubaan/gateway/blob/main/deploy/collector/README.md#what-an-instance-sends), [schema](https://github.com/vaubaan/gateway/blob/main/deploy/collector/schema.sql)).

**Addresses at the collector.** The collector runs on Vercel, as part of this site's project. Vercel records the client address of each request in the project's runtime logs and cannot be told not to; the project has no log drain and no Observability Plus, so an address stays in Vercel's logs for the plan's base window (one day on Pro, one hour on Hobby) and nowhere else. The collector's code never logs a request (its one log line is a count and a result) and forwards no header to its database, which therefore never sees an instance's address ([collector README](https://github.com/vaubaan/gateway/blob/main/deploy/collector/README.md#addresses-what-is-seen-and-how-long), [collector code](https://github.com/vaubaan/gateway/tree/main/apps/website/src/collector)). The same platform logs apply to visits to this site, which sets no cookie and runs no analytics or third-party script.

**How to check it.** The report is built by one constructor that refuses an extra key ([packages/telemetry](https://github.com/vaubaan/gateway/tree/main/packages/telemetry/src)); a structural test drives the real call pipeline with planted secrets and asserts that, at level `plugin-health`, nothing but the seven fields reaches the wire ([telemetry-report-shape.test.ts](https://github.com/vaubaan/gateway/blob/main/apps/gateway/tests/unit/structural/telemetry-report-shape.test.ts)), and unit tests hold the shapes of the `usage-stats` reports ([usage.test.ts](https://github.com/vaubaan/gateway/blob/main/packages/telemetry/src/usage.test.ts)); every column of the collector's tables is constrained to a closed vocabulary or a bucket ([schema.sql](https://github.com/vaubaan/gateway/blob/main/deploy/collector/schema.sql)). To guarantee that nothing is ever sent, set `telemetry.enabled: false` in the configuration.

**Plugin health** is published from these reports: a plugin version is deemed validated with at least 200 reports and 90 % of them successful, over the last 90 days ([plugin health](/plugins/health)).

## What is not covered

- **Pre-release, no audit, no certification**, as said above.
- **Out of scope of the threat model:**
  - The security of the host, the TLS terminator, the IdP and the provider accounts themselves.
  - An agent's own behaviour within its mission (what it does with what it is allowed to read).
- **Accepted risks** — known, decided, and written down with why each is accepted and what limits it ([threat model](https://github.com/vaubaan/gateway/blob/main/docs/handbook/threat-model.md#accepted-risks)):
  - A1: The operator can read decrypted credentials
  - A2: With `trusted_proxies` empty, address-keyed limits are shared.
  - A3: After a refresh-token replay, missions issued through the delegation keep serving
  - A4: Inside the refresh reuse window (30 s) a stolen refresh token replayed once is answered.
  - A5: Until its absolute lifetime ends (90 days by default), a delegation is renewed without the user being asked again.
  - A6: IdP group changes reach the instance at the user's next sign-in
  - A7: With `oauth.client_registration.policy: open`, `/oauth/authorize` redirects a signed-in user to a registered https URI on a parameter error without a click.
  - A8: The operator can take a connection: empty a local owner group, or deactivate its owner, then reassign it to themselves.
  - A9: The argument fingerprint is an unsalted SHA-256
  - A10: `-0` and `0` share a fingerprint.
  - A11: A share to a co-owner made before the rule existed still stands.
  - A12: An owner may share to a group they also belong to
  - A13: Installation binding proves access, not administration
  - A14: Scope caps of a catalog are not enforced in v5.0.
  - A15: Hostile content in a provider's answer reaches the agent as is.
  - A16: SQL classification is syntactic.
  - A17: During a registry outage a merge or release is not audited by that run.
  - A18: Railway: the agent tool cannot be bound.
  - A19: The SSH classifier models `sh`/`bash`.
  - A20: Denial of service by an authenticated party
  - A21: A mission that accepted `github.mint-install-token-clone` or `-push` hands its agent a GitHub installation token
  - A22: An IdP that sends no `email_verified` claim is believed on the email
  - A23: Accounts created before the store recorded an issuer are bound to the issuer of the first sign-in after the upgrade.
  - A24: A client that redirects to the loopback interface only is registered without the operator
  - A25: A mission's creator sees the email of every active user
  - A26: The surface rate limit of the execution surface (the MCP door and the git remote together) is one window per mission
  - A27: A mission does not expire unless its creator gives it an expiry
  - A28: Removing a sign-in provider does not deactivate its users.
  - A29: An address on `admin.emails` is admitted, and is an administrator, through any provider that verified it
  - A30: A token pasted by hand is a long-lived secret that acts as a person or a shared service account
  - A31: A push refused after its pack was stored — by the review, or waiting for an approval — has cost one upload to GitHub.
  - A32: The commits of a push are visible at GitHub under `refs/vaubaan/<random>` before the decision
  - A33: A mission key revoked or rotated while one of its git requests streams does not stop that request.
  - A34: A malicious dependency release not yet detected when its cooldown ends is installed
  - A35: A fix published by Debian waits for the Node image to be rebuilt
  - A36: The scanners' findings are read in the job log
  - A37: An outsider can revoke the bootstrap token
  - A38: A person connecting an account does not read the provider's own reason for refusing it

## Report a vulnerability

Do not open a public issue. Use GitHub's private vulnerability reporting: on the repository, the **Security** tab, **Report a vulnerability** ([direct link](https://github.com/vaubaan/gateway/security/advisories/new)); it is the project's only reporting channel. Include the version or commit, what an attacker starts from, the steps or a failing test, and the impact you believe it has; remove real credentials. The project aims to acknowledge a report within a few working days, keeps you informed in the advisory, and publishes it with credit once a fix is available; these are intentions, not commitments ([SECURITY.md](https://github.com/vaubaan/gateway/blob/main/SECURITY.md)).
