Skip to content

Security & trust

IronClaw's value is not "AI agents" — it is AI agents you can run without trusting them. This page is the map of that trust story: what IronClaw defends against, the invariants that make the defense hold, and how you verify what you install.

Start here

  • Threat model

    The assumption that drives every design decision: the agent inside the sandbox is potentially compromised. What that means for the blast radius, and — in §8 — what counts as a vulnerability.

  • Verify a release

    Every release is checksummed, keyless-signed with cosign, and carries build-provenance attestations. Here is how to check all three before you trust a download.

The invariants

IronClaw's hardening rests on a small set of invariants that hold regardless of what the agent does:

  • Sealed runtime. The sandbox has no interpreter, no in-sandbox package install, and no rootfs mutation. An agent can only invoke capabilities the binary already compiled in. This is why skills and MCP servers grant capabilities through configuration, never code.
  • Deterministic approval gateway. Every mutation — persona, enabled tools, packages, wiring, permissions, mounts, create_agent — is held at the gateway until a human approves it. There is no bypass path. The Quickstart makes this choke point concrete in two commands.
  • No public surface. The control-plane API binds only to the private mesh (Tailscale) interface; network reachability is the primary control, and the bearer token is defense-in-depth on top of it. See the API reference.
  • Network-isolated sandboxes. Sandboxes run network=none by default; outbound access is granted host-by-host through the egress broker, only as part of an approved change.
  • Append-only audit. Every approve/reject decision and every gated action is recorded. See Architecture.

Credential vault: agents use keys without holding them

An agent reaches a vaulted API by logical namevault://<cred>/<path> — and never by holding the key. The egress broker forwards the call to a separate host-side injector that attaches the real credential; the broker (and the sandbox) inject nothing. Access is deny-by-default and per agent group: a group may use a credential against a host only if an approved policy grant says so.

Those grants are config, never secrets — every rule names a credential, never holds one — and they are managed through the gateway like any other capability change, so a grant is held until a human approves it and is recorded in the audit log. Manage them with ironctl vault:

# See a group's deny-by-default state and active grants (no secret is ever shown):
ironctl vault list --group <agent-group>

# Propose a grant (held at the gateway for human approval):
ironctl vault grant  --group <agent-group> --credential github --host api.github.com --by you
ironctl change approve <change-id> --by you

# Narrow or remove a grant (also gateway-gated):
ironctl vault revoke --group <agent-group> --credential github --host api.github.com --by you

Rotating the secret value behind a credential is an injector operation — the control plane never holds the key, so there is nothing for it to rotate. Point the broker at an injector with --vault-endpoint. The threat model's §11 has the full model.

The supply chain is part of the promise

A release a user cannot verify is not a secured release. IronClaw's published artifacts are:

  • Reproducible — re-derivable from a known commit, with no nondeterministic inputs leaking into the build.
  • Checksummed — every archive is listed in SHA256SUMS; the installers (install.sh / install.ps1) verify checksums before executing anything.
  • Signed — a keyless cosign signature over SHA256SUMS is the trust anchor.
  • Attested — build-provenance attestations tie every artifact back to its source commit and the workflow that built it. Both the release archives and the individual binaries inside them carry provenance, so gh attestation verify works against either.
  • Proven contained — every release also ships a signed containment report (ironclaw_<version>.containment.json and .txt). The red-team escape harness runs against the offline demo at the released commit and freezes the result into a machine-verifiable artifact: every isolation invariant, the assertion that proved it, and pass/fail, bound to the commit and the runtime tested. The two files are checksummed into CONTAINMENT-SHA256SUMS, which is cosign-signed and attested the same way SHA256SUMS is — so you can confirm the threat model's core §5/§8 invariants held for the exact version you run, without re-running anything. If a core assertion had failed, the release would have been yanked, not shipped.

The Release runbook is the operational reference for cutting, verifying, and yanking a release. The threat model — the single source of truth for what IronClaw defends against — is threat-model.md; this page and SECURITY.md summarize and point there rather than restating it.

Fuzzing the trust boundary

The parsers that sit on the trust boundary — skill/MCP manifest YAML, the minisign signature and public-key blobs, and the name/version/asset-path validators that compose filesystem paths — all consume attacker-influenced bytes. Every one of them must fail closed: malformed input returns an error, never a panic (a crash in the host control-plane is an availability break), and a validator must never accept an identifier or path that could escape its mount root.

These properties are enforced by Go native fuzz targets in internal/host/skills/fuzz_test.go. Each target ships an inline seed corpus (valid inputs plus hostile ones — traversal paths, NUL bytes, YAML pathologies, junk base64) so coverage starts meaningful. Run one locally with:

# 30s is enough for a quick regression check; raise -fuzztime for a deeper run.
CGO_ENABLED=1 go test ./internal/host/skills/ -run='^$' -fuzz='^FuzzParse$' -fuzztime=30s

Available targets: FuzzParse (manifest), FuzzParseSignature, FuzzParsePublicKey, FuzzValidAssetPath, FuzzValidIdentifiers.

Two CI jobs keep this honest, both isolated from the release path so a fuzz failure can never gate a release:

  • The fuzz job in ci.yml runs every target for 30s on each push and PR — a fast regression smoke test (and what OpenSSF Scorecard's Fuzzing check detects).
  • fuzz-nightly.yml runs each target for several minutes on a nightly schedule (and on demand via Run workflow), giving the engine time to walk past the seed corpus. If a target crashes, the reproducer it writes under internal/host/skills/testdata/fuzz/ is uploaded as a build artifact for triage.

Reporting a vulnerability

Disclosure policy lives in SECURITY.md. The scope — what counts as a vulnerability — is defined in §8 of the threat model, which the disclosure policy points back to.