---
title: "Understand connection security"
description: "Review callback checks, node binding and the edge configuration a connection needs."
canonical_url: "https://docs.oz0.ai/admin/connections/security"
---
# Understand connection security

> Review callback checks, node binding and the edge configuration a connection needs.

For: cluster and tenant administrators.

Review callback checks, node binding and the edge configuration a connection needs.

## Where everything goes

1. Connect in the console (or `connection start`) asks the edge, through management, for the flow's URL: an address of the edge's own with a signed one-time state.
2. The browser opens it, and the edge sends it on to the provider: a redirect, or a page that posts the step's form (such as GitHub's app manifest). A guided step is a page of the edge's own instead, with its link, what to copy, its form for what you paste, and its checks; the form goes back to the edge (`POST /connect/<tenant>/<name>/step`).
3. The admin confirms on the provider's page, and the provider sends the browser back to `/connect/<tenant>/<name>/callback` on the edge.
4. The edge exchanges what came back (a code, an installation's ID) with the provider, server to server, and saves what the step keeps; then the next step, or back to the Connections tab.
5. Later, before each call the connection covers, the node's runtime asks the edge for that call's token, and the edge mints it with the provider.

### Store credentials at the edge

The steps end at the edge, on its HTTPS port, never at management, so the private key a provider hands out reaches only the edge, which seals it at once as a tenant secret only edges can open. What is not secret (an app's ID and name, the installation's account) is saved on the connection, where the console shows it.

### Single-use state and audit records

The state in each step's URL is signed by the edge and works once, for 15 minutes: a callback with another state, or the same one again, is refused. Every step is in the [audit trail](https://docs.oz0.ai/govern/audit-trail) as `connection.start`, `connection.step`, `connection.paste` (the names of the secrets pasted, never their values), `connection.check`, `connection.connected` or `connection.failed`, as the admin who started it.

### Scope and deliver call tokens

Each call's token is minted for the scope the plugin gives the function and for the resource its arguments name: with GitHub, the permissions one function needs, for the one repository the call names, for an hour at most. The edge reuses a token of the same scope and repository while it has more than ten minutes left.

Ending a call does not revoke its GitHub token. The token itself stays valid until expiry, up to an hour after issuance; protect it for that remaining lifetime.

The runtime hands the token to the tool in the call itself (MCP's `_meta`), never in its environment, on disk or in the job's history. With a [credential binding of the connection](https://docs.oz0.ai/admin/credential-injection#bind-a-connections-token), the tool gets only a placeholder there: the edge seals the token to the node that makes the call, and the node's egress proxy puts it on requests to the bound hosts alone.

### Restrict browser return addresses

A flow returns the browser only to the edge's own address, the web console's or one of the tenant's [CORS origins](https://docs.oz0.ai/build/apps/live-feeds) (in dev mode, this machine's own addresses too).

The console's trusted addresses are `--public-url`, the management port's advertised address and, for a flow started from the console, the address the browser used for it, whatever its name: the browser's `Origin` vouches for it, and management passes it on, signed, to an edge apart from it; `connection start --return-url` with any other address is refused, and a flow whose return address stopped being trusted while it ran ends on the edge's own page.

The edge's pages name where they send the browser: a step's form page names the provider's host (and its Content-Security-Policy lets the form go nowhere else), and the last page names the host its link returns to.

### Exchange a code once

The callback spends the step's one-time nonce before it exchanges the code, so a callback replayed from the browser's history or a log is refused and never reaches the provider again.

Where a provider takes PKCE, the step uses S256 only: the challenge goes through the browser, the verifier only into the edge's own exchange; a provider without PKCE gets the client's secret or the app's key in the exchange, so a code alone is worth nothing to someone who copied it.

GitHub's app manifest step is the one exception: GitHub's protocol has no client yet, and its one-time code reaches only the edge's HTTPS callback.

The console's own sign-in uses PKCE S256 too.

These are acceptance tests of the edge (`server/internal/connections/oauthflaws_test.go`), one per flaw of the research review's taxonomy; the two that concern an authorization server (open client registration, trusting an unknown client) pin that the edge is none.

### Tokens bound to the node

The edge mints a call's token only for the tool call it handed the asking node: the request carries that call's [binding](https://docs.oz0.ai/concepts/run-receipts/calls-and-charges#calls-bound-to-the-node), and the edge refuses one from another node or another certificate, for another job, for another function than the token is asked for, or with other arguments than the call's: the binding carries a digest of the call's short arguments as the edge handed it out, so a token is minted only for the repository the call itself names.

The job and the agent a token is minted for come from the binding's envelope, never from the node's word. So a token request replayed from another node, or a binding lifted from a sandboxed tool, gets nothing.

With `server start --call-binding required`, or the default `auto` once every online node's runtime carries bindings, a token request without a binding is refused too. See [Run receipts](https://docs.oz0.ai/concepts/run-receipts/calls-and-charges#calls-bound-to-the-node).

### Read fallback credentials at the edge

The fallback secret is read only by the edge too: switching a plugin from a token to an app changes nothing in the plugin or the agents that use it.

### Isolate connections by plugin

A connection belongs to its plugin. Another plugin cannot declare the same connection, take over one that was connected, or get what it saved by naming it in `requires.secrets`: the install is refused, and the edge gives those secrets to no other plugin's nodes. Nor can a plugin get them through a connection of its own whose name is a lookalike, such as `github-app` next to `github`: two connections whose secret prefixes nest (`GITHUB_` holds `GITHUB_APP_`) cannot be installed by different plugins of a tenant, and the edge refuses a connection every secret another plugin's connection names.

A connection's signing key, such as the GitHub App's private key, reaches no node at all, and is never sent in a request, only signed with.

## What the edge needs

Browsers and the other system reach the edge's HTTPS port: set the address they use with `--public-edge-url` on `server start` (default: `https://` and the edge's advertised API address). For GitHub, that address must also be reachable from GitHub for its webhooks, and have a certificate browsers and GitHub trust, such as a proxy with a public certificate in front of `/connect/` and `/events/`.

```bash [Terminal]
oz0 server start --public-edge-url https://oz0-edge.acme.se
```

The edge's own requests for a connection (exchanges, token requests and checks such as Slack's `auth.test` or Discord's `GET /users/@me`, to the addresses its params name, such as `api_url`; see [outbound connections](https://docs.oz0.ai/concepts/outbound-connections)) go to public addresses over https only, since a tenant admin sets those params. A GitHub Enterprise Server on your own network needs its network listed by a cluster admin; dev mode allows every address:

```bash [Terminal]
oz0 server start --connect-allow-net 10.20.0.0/16
```

Plugin authors declare connections as described in [Connections in a plugin](https://docs.oz0.ai/build/parts/connections).


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
