---
title: "Bind secrets to destination hosts"
description: "Set the host binding with the value. Managing a provider or plugin alone cannot widen it."
canonical_url: "https://docs.oz0.ai/admin/secrets/host-bindings"
---
# Bind secrets to destination hosts

> Set the host binding with the value. Managing a provider or plugin alone cannot widen it.

For: cluster and tenant administrators.

Set the host binding with the value. Managing a provider or plugin alone cannot widen it.

## Where a secret may be sent

A secret that leaves the edge for a host of someone's choosing goes only to a host it is bound to: a plugin source's
token (`--secret` on `plugin install`, for Git, an OCI registry or a marketplace), a workspace export's push and pull
request, a provider record's key or token endpoint, and a webhook notification's signature.

### Set allowed hosts

Give the hosts when you
set it, each `host` (that host on the default port), `host:port`, or `*.domain` (any host below the domain), up to 16.

A wildcard over a public suffix, a name many owners' domains share such as `*.co.uk` or a hosting service's
`*.github.io` (by the public suffix list the server ships with), is refused: name your own domain (`*.example.co.uk`)
or the host.

A wildcard reaches only the hosts its domain's owner holds: where the list names suffixes beneath the
domain, such as `s3.amazonaws.com` under `amazonaws.com` (every bucket under it is its owner's), `*.amazonaws.com`
covers `sts.amazonaws.com` but no host under those suffixes, nor the suffix itself; name such hosts exactly.

A
binding stored before this check still reads, and an entry over a public suffix sends the secret nowhere until you
set the secret again with its hosts:

```bash [Terminal]
oz0 secret set GITHUB_TOKEN --from-file ~/.secrets/github-token --host github.com --host api.github.com
printf %s "$KEY" | oz0 secret set ACME_LLM_KEY --host llm.acme.example
printf %s "$HOOK" | oz0 secret set DESK_HOOK_KEY --host desk.acme.example --signing   # signs webhooks, is never sent
```

### Permissions and refusal messages

The hosts are sealed with the value, so only the edges read them and nobody changes them without the value: setting a
binding takes `secrets.write` and the value, while installing a plugin, exporting the workspace, managing models or
notifications only names a secret.

A secret bound to no host goes nowhere, with one exception: a built-in provider's key
(`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`) goes to its own provider as the edges' operator set its address, and a preset's
key (`MISTRAL_API_KEY` and the others) to the preset's own address.

A refusal names the secret, the host and the command
that binds it, to someone who may list the tenant's secrets (`secrets.read`).

Anyone else who names a secret (a role
with only `plugins.install`, `workspace.write`, `models.manage` or `notifications.manage`) gets one and the same refusal,
"tenant secret NAME is not sent to HOST", whether the secret is missing, is a signing key, is bound to no host or to
other hosts, still before anything is sent; and what they read back later (a notification's delivery, a routine
output's error, a webhook's answer, the provider list, an install's warnings) says no more.

So naming secrets tells such
a role nothing about which ones the tenant has.

A credential goes to its hosts only over `https` (plain `http` only to the same machine, such as a
local model server on `127.0.0.1`): a binding names hosts, not schemes.

### Keep or replace hosts during rotation

A set without `--host` keeps the hosts the secret had (a rotation), as recorded with its current
value by a server that may set them: on a server that runs the edge role with the secrets key, only by an edge.

When
that record does not check out (a database row changed by hand, a record from an earlier value, a backup restored into
another cluster, or, on an edge, hosts set from a management that runs apart), the set is refused and says to give the
hosts again with `--host`.

A value an edge sets itself, which nobody can give hosts for (a connection's refreshed or
saved token), keeps hosts only an edge recorded and is otherwise bound to no host, which the audit log records
(`hosts_not_kept`); so where management runs apart, a binding set from management lasts until the connection next
refreshes.

`--unbind` clears them (the console's
"Bound to no host").

### Use a signing-only key

A signing key
(`--signing`) is only for a webhook target or a routine output on one of its hosts, or for checking the requests of a hook (a routine's or a flow's webhook, an event source) bound by name to the edge's own address its senders post to, with the port (`--host oz0-edge.acme.example:7443`: the edge API's advertised address, its `--public-edge-url` or a `--san` name), and no sink ever sends it.

A hook's key is bound to the edge's own addresses alone: one bound to another host too (a webhook target's) signs what goes there, which that host could send back as a request, so no hook takes it.

A hook takes nothing else, except a secret a connection of the hook's own plugin saved; a secret in your own store is no hook's key.

An event source whose plugin checks each delivery's signature itself (`scheme: plugin`) is the exception: the edge compares nothing with its secret, so it takes the secret the plugin lists however it is bound, and only needs it set.

The console's Secrets tab
sets and shows the same.

### Store and plugin exceptions

A secret in your own store goes to the hosts its store lists on the edges (`hosts` in the
store's entry, below), and takes no hosts of its own.

A connection's tokens and the secrets plugins read are not sent
by these sinks: plugins get theirs as before, on their nodes, and no plugin gets a model provider's key, neither as a
declared secret nor in a tool's argument.

A secret a plugin lists is the plugin's to use as its code does: installing
the plugin hands it over, wherever the secret is bound.

A connection's token set by hand, such as the fallback
`GITHUB_TOKEN`, goes to the plugin's own address bound or not, and to any other address its connection's params name
only when it is bound to that host ([where credentials go](https://docs.oz0.ai/admin/connections/destinations#where-credentials-go)).

### Remove a secret in use

A deletion is refused while something uses the secret, and the refusal lists what: plugins that declare it (`requires.secrets` or `optional_secrets`) or fetch their repository with it, providers that read it as a key, bindings that inject it, the connection that saved it, and routines whose webhooks or outputs name it, each with its owner. `oz0 secret dependents NAME` lists the same without deleting anything. Point them at another secret first, or delete it anyway with `--force` and a `--reason`, which the audit log keeps with the list (`secret.delete.forced`):

```bash [Terminal]
oz0 secret dependents GITHUB_TOKEN
oz0 secret delete GITHUB_TOKEN --force --reason "revoked at GitHub; plugins moved to GITHUB_APP_KEY"
```

## Rebind older inbound webhook keys

A routine or flow webhook, or an event source, verifies requests only with a signing key bound to the edge's own addresses and no other host. A plugin connection may also supply a saved secret that it does not send as a credential.

A key previously set as an ordinary secret is refused for this purpose. Deliveries return `401`, and the edge log names the key and reason. Set it again with `--signing` and the edge host and port, for example `oz0 secret set NAME --signing --host edge.example.com:7443`. There is no separate list of keys requiring this migration.


## Sitemap

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