# Security (/ship/security)

## Credentials [#credentials]

Provider keys live in `.env` or the environment, **never in `config.cuelo`**.
`config.cuelo` is meant to be committed. It references secrets as `${VAR}`
(store URLs, HTTP tool URLs and headers, API keys), and the schema rejects
literal keys where it can. Commit `.env.example`. Keep `.env` in `.gitignore`,
which `cuelo init` sets up.

## API keys [#api-keys]

Without keys the server is open. That's fine on `127.0.0.1` but not on a
network. Turn keys on:

<Tabs items={["Project","Default endpoint"]}>
<Tab value="Project">
```yaml title="config.cuelo"
serve:
  host: 0.0.0.0
  api_keys: ["${CUELO_KEY}", "${CUELO_KEY_OLD}"]   # several for rotation
```

```bash title=".env"
CUELO_KEY=sk-cuelo-...
```
</Tab>

<Tab value="Default endpoint">
```bash title=".env"
CUELO_API_KEY=sk-cuelo-...          # comma-separate several
```
</Tab>
</Tabs>

<Callout type="warn">
The server refuses to start on a non-loopback host (for example `0.0.0.0`) when no key is set.
</Callout>

With keys on, every request needs one, except `/health` and the dashboard's
static page files (the page asks for a key, then sends it). Clients can send it
any of these ways:

| How                                                   | For                                         |
| ----------------------------------------------------- | ------------------------------------------- |
| `Authorization: Bearer <key>`                         | OpenAI SDKs (`api_key=`), servers           |
| `x-api-key: <key>`                                    | servers, `curl`                             |
| WebSocket subprotocol `openai-insecure-api-key.<key>` | browsers, which can't set WebSocket headers |
| `?key=<key>` in the URL                               | links, such as recording downloads          |

A missing or wrong key gets HTTP `401` with an OpenAI-style error body, or
WebSocket close code `4401`. `GET /v1/auth` returns `200` or `401`, so a client
can tell a refused WebSocket from an engine that isn't running. Keys are
compared in constant time.

<Callout title="Browsers and keys">
A key in a browser is visible to whoever uses that browser. For public web apps, keep the Cuelo
key on your server. Have your backend open the Realtime connection, or put a gateway in front that
issues short-lived tokens.
</Callout>

## CORS [#cors]

Browsers on other origins can only call the API if you allow them:

```yaml title="config.cuelo"
serve:
  cors: ["https://app.example.com"]
```

For the default agent, use `CUELO_CORS=https://app.example.com`. Empty means
same origin only, which covers the built-in dashboard.

## Call data [#call-data]

Calls store audio, transcripts and tool arguments, which may include personal
data. Point `store.url` at a database you control, set `store.audio` to
encrypted storage, and delete old sessions according to your retention policy.
Set `summary: false` if summaries shouldn't be generated.