Skip to main content
Workflows run attacker-influenced inputs by nature: they fetch URLs, shell out, and feed model output into tools. Nika’s position is that the mechanism must be safe before policy makes it configurable: the dangerous primitives ship hardened by default, and a violation is a typed NIKA-SEC-* error your workflow can read, not a stack trace.

The threat model in one table

Permits β€” declare the blast radius

On top of the hardened mechanisms, a permits: block makes the file itself declare its blast radius. Four families, one shape:
Two laws govern it:
  1. No permits: block β†’ the engine floor only. The hardened defaults below (SSRF defense, shell blocklist, secret masking) apply β€” nothing else is restricted.
  2. Any permits: block β†’ default-deny for the whole file. Anything not listed is refused. A task reaching beyond the boundary is caught by nika check, statically β€” NIKA-SEC-004, with the exact fix β€” and the same boundary is enforced again at run time.
You never have to write it by hand: nika check --infer-permits computes the tightest boundary the file actually needs and prints it, ready to paste. The SSRF floor below stays on regardless of permits β€” a permitted host still cannot bounce into private space.

SSRF defense (on by default)

nika:fetch refuses to be bounced into private space. Three layers, verbatim from the engine (nika-http):
1

Static checks (pure)

Scheme allow-list (http/https only), blocked hostnames (localhost, cloud metadata), literal-IP range checks: loopback, RFC 1918, link-local/metadata (169.254.169.254), CGN, IPv6 local ranges, v4-mapped v6.
2

DNS resolution check

Non-literal hosts are resolved and every address is range-checked: this kills decimal-IP tricks (http://2130706433/) and public names that resolve to private addresses.
3

Per-hop redirect re-check

Client-level redirects are disabled; the engine follows redirects itself and re-runs layers 1+2 on every hop. A public host cannot 302 the client into your VPC.
Response sizes are capped (64 MiB default) and self-signed TLS is rejected by default. Private-network access is opt-in configuration, never the default.

exec: the data-channel rule

exec in shell mode runs every command against a blocklist before spawn. But the structural defense is in how you write workflows: tainted data goes through data channels, never code channels.
Passing tainted data to a privileged sink without going through a data channel raises a typed taint violation: taint is a property of the data, not of who runs the workflow.

The two authored doors

Some legitimate workflows do need to cross one of these lines. Nika refuses to let that happen silently: there are exactly two doors, both authored, both requiring a written justification, both recorded in the run receipt.
Each door lifts one law and nothing else. declassify: does not widen permits: β€” the value is still matched against the declared boundary, so it can never become a permit bypass, and there is no implicit declassification. inert: does not touch the net.http boundary or the SSRF floor. Because both are plain YAML keys, grep -rn 'declassify:\|inert:' over a repo returns every place a law was waived, with the reason attached. That is the point: the escape hatch is auditable by construction.

Trust levels + prompt injection

Content entering a workflow carries a trust level. Untrusted content (fetched pages, user comments) is tracked as it flows into prompts and tools: The three codes the spec REGISTERS today: NIKA-SEC-001 (exec blocklist hit), NIKA-SEC-002 (agent tool outside the whitelist), NIKA-SEC-003 (run-recursion bound). The runtime trust layers above are engine-side (the Shield): they emit within the NIKA-SEC namespace as they land. Every failure is a typed error with a stable code and a transient flag. Your on_error: can act on it.

Secrets

${{ secrets.* }} is a first-class namespace (vault/env/file-backed). Secret values are masked in logs and traces at the binding layer: they never appear in the event stream, and they are not readable back from a task’s recorded output.

Supply-chain hardening (the engine itself)

  • unsafe_code = "forbid" workspace-wide Β· zero .unwrap() in src/ (CI-enforced)
  • cargo deny on every PR Β· cargo audit on every dependency change
  • Sealed kernel traits Β· external code cannot impersonate engine I/O
  • lib tests Β· clippy warnings Β· every crate passes 12 admission gates
  • AGPL-3.0-or-later Β· the source travels with the binary, always auditable

Reporting a vulnerability

security@supernovae.studio (please not via public issues). Acknowledgement ≀72h, triage ≀7 days, disclosure ≀90 days. Full policy, disclosure process and the out-of-scope list: SECURITY.md.

See also

Error codes

The NIKA-SEC namespace + every typed failure.

Bindings

Taint, scopes, and the secrets.* namespace.

Builtins Β· fetch

The nika:fetch contract (engines MUST ship SSRF defense).

SECURITY.md

Reporting, disclosure timeline, hall of fame.