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

SourceHome
SCAFO_HOME, when setthat 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.

PathModeContents
<home>/config.jsonc0600The 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>.json0600One 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

~/.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:

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

--key <value>
SCAFO_OPERATOR_KEY
the key of the remote this invocation resolved to
./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

--remote <name>
SCAFO_REMOTE
the remote pin in the config file
the only remote in the home, when there is exactly one
an interactive prompt, when a terminal is attached
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

--node <url>
the first endpoint carried by a portable key file, when one was given
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

VariableEffect
SCAFO_HOMEThe 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_REMOTEThe remote every command acts on, unless --remote says otherwise.
SCAFO_OPERATOR_KEYAn 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 in Config.

#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.

CommandBucket pathOther flags
scafo fleet nodesalways--json
scafo metricsalways--name, --env, --since, --json
scafo tracesalways--name, --env, --since, --limit, --all, --json
scafo trust set / show / removealways--name, --env, --issuer, --audience, --subject
scafo wg init / peersalways--node-id, --state-dir, --transport, --listen, --advertise, --wg-address, --wg-endpoint, --wg-port, --stdout
scafo operator keygen --registeronly with --register--out, --id, --role
scafo remote add --registeronly 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 / deleteonly when no node is named--name, --env, --node, --remote, --key, and each subcommand's own
scafo deployment prunealways--name, --env, --keep, --older-than, --yes, --dry-run
scafo healthreads 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 in Docs > Operators.

#Portable remotes

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

{
  "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.