Skip to content

Vault and secrets

Every config file refers to a credential the same way: ${NAME}. Where that NAME resolves is the vault chain's job. The chain has two tiers:

  • Simple tier — no setup at all: environment, then an owner-only file.
  • Secure tier — a backend whose server enforces who may read what.

The configuration shape is identical in both; only config/vault.yaml changes.

flowchart LR
    P["${ARXIV_API_TOKEN}"] --> ENV["env"]
    ENV --> FILE["secrets.local.sh<br/>0600"]
    FILE --> SOPS["sops<br/>encrypted file"]
    SOPS --> CMD["command<br/>pass / op / bws"]
    CMD --> BAO["openbao<br/>server-enforced"]

First backend that holds the key wins.

The chain

config/vault.yaml — copied to $MINI_ORK_HOME/config/vault.yaml — lists the backends in precedence order:

backends:
  - type: env
  - type: file
  # - type: sops
  #   file: config/secrets.enc.yaml
  # - type: command
  #   argv: ["pass", "show", "mini-ork/{key}"]
  # - type: openbao
  #   address: http://127.0.0.1:8200
  #   mount: secret
  #   token_env: OPENBAO_TOKEN
  #   paths:
  #     - mini-ork/collector
  #     - mini-ork/reviewer

If the file is absent, the chain is simply env then file. That is the whole local story — no daemon, no server, no account.

Backends

Type Holds Best for Notes
env Process environment Debugging, CI injection Keep it first so one export rotates a value for one invocation
file secrets.local.sh A single laptop Default path $MINI_ORK_HOME/config/secrets.local.sh; override with MINI_ORK_SECRETS
sops An encrypted YAML document Git-committable team secrets age or PGP recipient; still no server
command Any executable printing one value for a key pass, op, bws, infisical {key} is substituted into one argv element, never through a shell
openbao Self-hosted OpenBao (the MPL-2.0 OpenBao fork of Vault) Shared, audited, cloud-scale runs The server enforces which paths a token may read

file backend details

- type: file
  file: config/secrets.local.sh     # default: $MINI_ORK_HOME/config/secrets.local.sh

The file is parsed, never sourced and never executed. The parser accepts simple export NAME=value records only. Keep it owner-only (0600); the file is gitignored by the **/secrets.local.* rule.

Never commit real keys. The template is config/secrets.example.sh.

command backend details

- type: command
  argv: ["infisical", "secrets", "get", "{key}", "--plain", "--silent"]

The command receives one value on stdout. {key} is substituted into an argv element, so a key name containing shell metacharacters cannot become a command.

openbao details

OpenBao is the recommended secure tier because least privilege is enforced by the server, not by mini-ork:

  • Prefer a short-lived, path-scoped token over a root token.
  • Issue one KV-v2 path per node role; a collector's token cannot read the reviewer's path.
  • The token itself can live in OPENBAO_TOKEN, or in the vault — but then something still has to hold that.

Fail-closed behaviour

A missing credential is an error, not an empty string. If a config references ${NAME} and no backend has it, the operation fails with a clear message naming the variable. Nothing silently dispatches with an empty key.

The one deliberate exception is a lane whose kind does not need a key: anthropic-native with an ambient Claude Code login, codex-native, opencode-native. Those are allowed to be unkeyed by design.

Inspecting without leaking

mini-ork vault status           # which backends are wired, and are they ready
mini-ork vault which NAME       # which backend holds that name

Both commands print names and locations only. No value ever leaves through a command's stdout.

To set a key interactively:

mini-ork providers configure <lane>
mini-ork providers configure --workflow <workflow.yaml>

configure discovers the keys a lane or workflow needs, prompts securely, and writes secrets.local.sh atomically with 0600 permissions. For automation, pipe NAME=value records to configure --from-stdin <lane> — never pass a key as a command-line argument, where it lands in shell history and process lists.

Using a variable

Anywhere a config value is read, the ${NAME} form is resolved at the boundary:

env:
  ARXIV_API_TOKEN: ${ARXIV_API_TOKEN}
  OPTIONAL_FLAG: null        # declare the variable absent for this node

A node's env: declarations are resolved from the secret store at the node boundary. A run-scoped config/env.yaml overrides them.

Rotation

  1. Update the value in the highest-precedence backend you control.
  2. Run mini-ork vault which NAME to confirm which backend now answers.
  3. Restart any long-lived process that cached the old value.

An environment export wins for that invocation only — which is exactly what you want while rotating: prove the new value works, then persist it.

Leaked credential protocol

If a key reaches a terminal transcript, a log, or a commit, treat it as exposed. Rotate it at the provider first; deleting the text afterwards does not undo the exposure.