
# Operator configuration

> Where the CLI keeps its remotes and keys, how one invocation decides who it speaks as and where it speaks, and which commands read the bucket instead of a node.

The same `scafo` executable is both the node daemon and the operator CLI. This page is about the CLI half: the state it keeps for a person, rather than the state a node keeps for a machine.

## The operator home

| Source | Home |
|---|---|
| `SCAFO_HOME`, when set | that path, which **must be absolute** |
| `XDG_CONFIG_HOME`, when set to an absolute path | `$XDG_CONFIG_HOME/scafo` |
| otherwise | `~/.scafo` |

A relative `SCAFO_HOME` is refused with `invalid-input` rather than resolved against the working directory: commands are run from wherever you happen to be standing, and a home derived from that would be a different set of remotes on the next invocation.

| Path | Mode | Contents |
|---|---|---|
| `<home>/config.jsonc` | `0600` | The remotes this operator can reach. JSONC, so it can be commented; managed by `scafo remote`, which does not preserve hand-written comments. **No secret lives here** — a remote only refers to its key. |
| `<home>/keys/<name>.json` | `0600` | One operator keypair. The private halves are here and nowhere else. |

The home directory itself is created `0700`. Nothing in it is ever written by a node daemon, and nothing in it belongs in a repository.

Remote and key names are slugs: 1–63 lowercase alphanumeric characters, optionally separated by single hyphens. The name is both the remote's name and the file its key lives in.

### What a remote holds

```jsonc [~/.scafo/config.jsonc]
{
  "v": 1,
  "remotes": {
    "prod": {
      "endpoints": ["https://node-a.internal.example.com:7400"],
      "key": { "kind": "file", "path": "keys/prod.json" }
    }
  }
}
```

A key reference is relative to the home unless it is absolute, so a home that is copied or moved still points at its own keys. A remote may carry a second, `rotating` reference while `scafo operator rotate` is in flight.

`scafo remote list` prints the same thing readably; `scafo remote add` registers one, `scafo remote remove` forgets it — and forgetting a remote says nothing to the installation, whose registry still trusts the key until it is revoked there.

## Pinning a remote to a project directory

The **node** config file — `./scafo.config.jsonc`, then `./scafo.config.json`, or whatever `--config` names — is read by operator commands too, for one key at its top level:

```jsonc [scafo.config.jsonc]
{
  "remote": "prod"
}
```

That pin holds no secret and belongs in the repository. It is only consulted when neither `--remote` nor `SCAFO_REMOTE` said which remote to act on. A malformed file, or a file without a `remote` key, is simply no pin — operator commands do not fail on it.

## How one invocation resolves

Two things are decided separately: **who is speaking** and **where**.

### The key

1. `--key <value>`
2. `SCAFO_OPERATOR_KEY`
3. the key of the remote this invocation resolved to
4. `./scafo-operator.json`, only when the home holds no remote at all

`--key` and `SCAFO_OPERATOR_KEY` accept the same three forms: a key file from `scafo operator keygen`, a portable file from `scafo remote export`, or an inline `base64:` value of one. The `SCAFO_OPERATOR_KEY` form is the CI shape — one secret string, no files to place.

The key is read **only when something actually signs**. A command that turns out to reach the bucket rather than a node never needs one and never fails for the want of one.

### The remote

1. `--remote <name>`
2. `SCAFO_REMOTE`
3. the `remote` pin in the config file
4. the only remote in the home, when there is exactly one
5. an interactive prompt, when a terminal is attached
6. otherwise `invalid-input` ("No remote chosen"), listing the remotes there are

A name nothing answers to is reported as a typo (`not-found`), not fallen back from. When a key was given outright with `--key` or `SCAFO_OPERATOR_KEY`, an ambiguous home is left undecided instead of refused: such a command already knows who it is, and the remote would only have added an endpoint.

### The endpoint

1. `--node <url>`
2. the first endpoint carried by a portable key file, when one was given
3. the first endpoint of the resolved remote

`--node` takes the base URL of a node daemon, such as `http://127.0.0.1:7400`. A command with no `--node` and no remote to supply one refuses rather than guessing.

### Presenting a token instead of signing

`scafo deploy --token <bearer>` presents a token instead of signing one with an operator key. A single-use **node token** is spent by the request that presents it, so a deploy started with one cannot also watch its status; a workload token the application trusts is not spent and watches the deploy to its end.

## Environment variables the CLI reads

| Variable | Effect |
|---|---|
| `SCAFO_HOME` | The operator home outright. Must be absolute. |
| `XDG_CONFIG_HOME` | `$XDG_CONFIG_HOME/scafo` becomes the home when `SCAFO_HOME` is unset and this is absolute. |
| `SCAFO_REMOTE` | The remote every command acts on, unless `--remote` says otherwise. |
| `SCAFO_OPERATOR_KEY` | An operator key outright: a path to a key file, a path to a portable file, or an inline `base64:` value. |

Commands that reach the bucket additionally read the node's own bucket variables — `SCAFO_S3_ENDPOINT`, `SCAFO_S3_REGION`, `SCAFO_S3_BUCKET`, `SCAFO_S3_PREFIX`, `SCAFO_S3_ACCESS_KEY`, `SCAFO_S3_SECRET_KEY`, and the `S3_ENDPOINT` / `AWS_*` fall-backs — resolved exactly as `scafo sail` resolves them, config file included.

::read-more{to="/config"}
The full precedence table for those settings.
::

## Commands that read the bucket directly

Some commands do not talk to a node at all: they open the registry themselves with the bucket credentials, which is what an installation with no node running still has. Except where noted below, they take the seven flags `--config`, `--endpoint`, `--region`, `--bucket`, `--prefix`, `--access-key` and `--secret-key`, plus their own.

| Command | Bucket path | Other flags |
|---|---|---|
| `scafo fleet nodes` | always | `--json` |
| `scafo metrics` | always | `--name`, `--env`, `--since`, `--json` |
| `scafo traces` | always | `--name`, `--env`, `--since`, `--limit`, `--all`, `--json` |
| `scafo trust set` / `show` / `remove` | always | `--name`, `--env`, `--issuer`, `--audience`, `--subject` |
| `scafo wg init` / `peers` | always | `--node-id`, `--state-dir`, `--transport`, `--listen`, `--advertise`, `--wg-address`, `--wg-endpoint`, `--wg-port`, `--stdout` |
| `scafo operator keygen --register` | only with `--register` | `--out`, `--id`, `--role` |
| `scafo remote add --register` | only with `--register`, and from `--config`, the config file and the environment alone — it takes no other bucket flag, because its `--endpoint` names a node | `--endpoint`, `--key`, `--from`, `--role`, `--config` |
| `scafo app retire` / `unretire` / `selector` / `replicas` / `delete` | only when no node is named | `--name`, `--env`, `--node`, `--remote`, `--key`, and each subcommand's own |
| `scafo deployment prune` | always | `--name`, `--env`, `--keep`, `--older-than`, `--yes`, `--dry-run` |
| `scafo health` | reads the settings, not the bucket | `--listen`, `--transport`, `--wg-address` |

Those five `scafo app` verbs are the ones that go both ways: a node is asked whenever one is named — by `--node`, or by the remote this operator's home resolved — and they fall back to writing the bucket otherwise. `scafo app list`, `show`, `block`, `unblock` and `label` have no bucket path at all: they are operator-key acts, and they refuse when no node is named.

::warning
Bucket credentials are root authority over the whole installation. A command that takes them is not scope-checked by anything; the operator-key path through a node is.
::

`scafo operator keygen` writes the pair to `--out`, or to `./scafo-operator.json`, owner-readable. With `--register` it records the public half in the registry bucket-side, which is the bootstrap path for the first admin key — there is no node to ask yet. `scafo remote add --register` does the same for a key it just minted into the home. Every other `scafo operator` subcommand goes over the wire to a node.

::read-more{to="/docs/operators"}
Keys, tiers and enrolment.
::

## Portable remotes

`scafo remote export <name>` writes one remote out as a single self-contained JSON document:

```jsonc
{
  "v": 1,
  "kind": "scafo-operator",
  "remote": "prod",
  "endpoints": ["https://node-a.internal.example.com:7400"],
  "key": { /* the operator keypair, private half included */ }
}
```

With `--out <file>` it is written owner-readable and the path is printed. Without one it goes to stdout, and a warning is printed first when stdout is a terminal, because the document holds a private key in the clear — pipe it into a secret store instead.

The same document is what `scafo remote add --from <path|base64:...>`, `--key` and `SCAFO_OPERATOR_KEY` accept, which is how one operator's access moves to another machine or into CI.
