Skip to content

Secrets

Soul Stack handles secrets by two rules: they are resolved on the Keeper side (the agent carries no Vault client) and masked on every output (logs, OTel, the UI, reports). Secrets come from Vault.

Vault is used in two capacities:

  • PKI — issuing and signing the agents’ identity certificates (SoulSeed). The certificate authority’s private key lives in Vault and never reaches the cluster’s disks (see Transport → Shared PKI root).
  • KV — a store for secrets (passwords, tokens, keys) that are injected into configs when a Destiny is rendered.

The key invariant: secrets never materialize on the Keeper cluster’s disk — they live in Vault and are pulled in at render time.

All calls to Vault happen on the Keeper, during task rendering — before the tasks go out to the agent.

  • The Keeper resolves secret references in the config (for example, an expression reading a value from Vault KV) itself and substitutes the real value into the already-rendered task.
  • The finished result is what goes to the agent. The agent carries no Vault client, there are no Vault tokens on the host, and the agent has no right to read anyone else’s secrets.

This follows directly from the thin-agent principle: the fewer privileges and less code on the managed host, the smaller the attack surface. Resolving secrets is an operation that reaches out externally, and it stays entirely on the Keeper.

Inside the render engine a secret is handled as an ordinary value — otherwise the legitimate scenario of “injecting a password into a module parameter” would not work. Leak protection is applied on output, at every boundary where data leaves the system:

  • logs — a step’s rendered parameters in log entries;
  • OpenTelemetry traces — span attributes;
  • the UI and API responses;
  • run reports.

A value marked secret in its source schema is masked at each of these boundaries — the secret never leaves in cleartext.

CategorySource of secrecyCan it be forgotten
Schema-based (secret: true)The field is marked secret in the scenario’s input-parameter schema.Yes — it depends on what the operator marked in the schema.
Sensitive-by-constructionThe parameter is secret by its very nature, regardless of marking (for example, request HTTP headers that routinely carry Authorization/Cookie). Baked into the module implementation.No — it can’t be turned off.

For the sensitive-by-construction category the value is never logged, never placed in OTel attributes, never returned in the result, and never included in run events — regardless of whether the operator marked it secret.

Beyond schema-based masking, a step can be explicitly marked “do not log” via the no_log key. This is an operator’s tool for steps where even the rendered parameters must not reach the outputs in full — an extra layer on top of the automatic masking of secret values.

Modules that reach out to the network (core.url, core.http) form a distinct risk boundary (SSRF, supply chain). The principle here is the same as everywhere in the system: our code is safe by default; relaxing it is an explicit operator choice.

Enabled by default:

  • only https:// (not http://, not file://);
  • protection against requests to internal addresses (an SSRF guard on the actually resolved IP);
  • TLS chain verification.

A specific control can be relaxed with explicit per-call flags — allow_http, insecure_skip_verify, allow_private. Each is off by default, relaxes exactly one independent control, and enabling it produces a warning in the run report — the relaxation is visible and auditable. This does not forbid capabilities; it is a safe default behind an explicit, recorded opt-out.