Status: Experimental. This project binds to
127.0.0.1by default. It is experimental and makes NO compliance claims. Localhost-only default, single-tenant v1. No certification, audit result, or conformance statement is claimed anywhere in this document.Purpose: The honest operator-facing security overview. It states what an operator is protecting, who the adversaries are, where the trust boundaries sit, what this system does not defend against, and — plainly — what an attacker who already owns the host can still do.
Primary sources: the ASI0X threat model (
sentinel/docs/security/threat-model-asi0x.md), the Gateway arming runbook (gateway/docs/runbooks/arming-authorization.md), and the per-productSECURITY.mdfiles (sentinel/,gateway/,council-rs/). Where a mitigation is not yet built, this document says GAP and names the roadmap phase that closes it — it does not imply a control that does not ship today.Claim-by-claim enforcement map: every public security claim, mapped to the code that enforces it (or honestly marked as not yet enforced), lives in
docs/security-claims-vs-reality.md.
The Sovereign Triad — product brand Irinity, shipped as the IRIN
mono-repo (irinityhq/irin) — is a local-first agent
control plane. Deterministic Sentinel watchers (no LLM in the watch loop;
fire decisions under a runtime-enforced ≤100ms budget — runtime.rs
constants with per-phase sub-budgets: observe 50ms, interesting 10ms, escalate
100ms, 200ms total pipeline, violations surfaced as BudgetViolation) observe
files, queues, feeds, and ledger deltas and emit
evidence-only Escalations. The Gateway routes, meters, and enforces
budget caps, then carries an Escalation to Council as a model: council-triage deliberation. Council is the decision authority — it deliberates
across multiple models and emits a bounded Directive, which the Gateway
signs (Ed25519 over RFC 8785 JCS-canonicalized bytes) into an append-only
directive_outbox whose companion fire record binds it into a SHA-256
hash chain. In v0.1 the signed outbox row is the
endpoint: Worker execute is not shipped. Turning the loop from dark to live
requires a dual-custody arm ceremony — a host bearer token plus a live
hardware attestation (Secure Enclave / Touch ID, or a FIDO2 key). Default is
OFF; everything binds to loopback.
The design maxim: Watch is cheap. Thought is rare. Action is final. Each tier earns the next. The security posture follows from it — cheap deterministic observation, expensive deliberation gated behind budget, final action gated behind hardware-attested human confirm.
Internal hash-chain genesis labels retain the InnerWay-era string
(innerway.watch.v0.1.genesis); they are frozen constants — renaming them
would invalidate every existing chain. A protocol-version bump is the
scheduled path.
| Asset | Where it lives | Why it matters |
|---|---|---|
| Signing keys — two separate materials | (1) Ed25519 ledger seed at ~/.irin/ledger_key.pem (make ledger-key-dev) — signs audit_events/ledger rows. (2) Sidecar directive identity key (directive_identity.json at DIRECTIVE_IDENTITY_PATH, default /var/lib/sidecar/ inside the sidecar container volume — not under the operator home; kid sidecar-v1-…) — signs directive_outbox envelopes (JCS + Ed25519). |
Whoever holds a given key can produce rows on that surface that verify cleanly. Forging a verifiable outbox row requires the directive identity material, not (only) the ledger seed. Highest-value on-disk secrets — see §5. |
| Arm-attestation key material | Secure Enclave key (~/.config/gateway/arm-attest.key); enrolled public-key registry arm_attest_keys.json (GW_ARM_ATTEST_KEYS_PATH) |
The private key never leaves the enclave — the only signing-relevant secret that cannot be read off disk (subject to the physical-access and enrollment caveats in §3). It is the residual control that gates going live. |
| Ledger / outbox integrity | Append-only stores with a deliberate asymmetry: watch_fires = SHA-256 hash chain only (high-volume evidence, unsigned); audit_events + directive_outbox = hash chain + Ed25519 signatures. Pruning/compaction cryptographically forbidden (rc1-audit-chain-pruning-ban) |
Tamper-evidence records — any edit to recorded history is detectable, not that history cannot be appended to (see §5). The two surfaces do not provide identical properties; see the provenance-asymmetry notes in the Gateway contract docs. |
| Council transcripts / precedent store | council-rs precedent engine (sessions/index.jsonl, Cold Eyes R2+ injection); per-tenant Librarian memory |
A poisoned precedent corpus slow-drips biased rulings into every future deliberation. This store is not integrity-watched today (V1 GAP). |
| Budget / spend ledger | spend_ledger (atomic reserve-settle-or-rollback); DAILY_SPEND_CAP ($50/UTC-day, proposed canary value), MAX_FANOUT_COST_USD ($5/directive) |
The hard ceiling on financial blast radius. The day cap is the binding control; single-step reserve is fail-closed on any DB error. |
| Arm state | arm_audit chain; writer_claim singleton row; default-OFF env manifest (WATCH_PRODUCER_ENABLED, WATCH_DISPATCHER_ENABLED, GW_ENABLE_COUNCIL_ENDPOINT) |
Whether the loop can act at all. Config-presence is execution-authority on the boot env-arm path — a copy-pasted .env with the live-switch set goes live. |
| API keys | Virtual gateway keys (gw_live_…); host arm bearer(s) in GW_ARM_PRINCIPALS; upstream provider keys (in ~/.zshrc, never hardcoded) |
Caller identity, arm authority (custody domain 1), and the provider credentials the Gateway proxies with (and strips from client requests). |
Mapped to the OWASP Top 10 for Agentic Applications (ASI0X) as enumerated in
threat-model-asi0x.md, cross-referenced to NIST AI RMF functions (Govern /
Map / Measure / Manage). Status is stated honestly per vector: Covered,
Partial, or GAP. Two vectors have no working mitigation today and are
called out first so no reader misses them.
-
V1 — Memory & context poisoning: GAP. An attacker who can write to the precedent corpus or a vector/memory store can slow-drip biased rulings into every future deliberation without touching any prompt or UI. The append-only hash chain protects the fire log and the outbox; it does not watch memory stores for drift. The multi-stage decontaminator screens inbound prompts, not retrieved content. Closing work is Phase C1 (
memory-integrity-watch), and it is hard-gated on first naming the watched store — no store, no sentinel. (NIST AI RMF: Measure gap on the knowledge-authority layer.) -
V4 — Sequence-level / multi-step attacks: GAP. Each escalation may pass policy, each directive may validate against
directive_fence, each spend may sit under the budget envelope — yet a crafted sequence (many small spends, a chain of tool calls, a velocity spike acrossdirective_outboxrows) can sum to an unapproved outcome that no single-step check catches. There is no stateful watcher over the sequence today. Closing work is staged B (named) → C0 (sequence-watch-assessment.md) → D (sequence-watch, a stateful, LLM-less sentinel). (NIST AI RMF: Manage gap.)
| Vector | Status today | What mitigates it (or why it's open) | Closing phase |
|---|---|---|---|
| V1 Memory & context poisoning | GAP | Audit chain protects fire-log/outbox, not memory stores; decontaminator screens inbound not retrieved | C1 memory-integrity-watch (after naming the store) |
| V2 Inter-agent trust boundaries | Partial | Directives Ed25519-signed + validate_directive_proposal_v1 shape-validated before signing; escalation locked to one route (council-triage, no bespoke REST endpoint); Worker execute not shipped (no privilege-bearing sub-agent to compromise). GAP within: no capability-token authority attenuation |
D capability tokens + Gateway directive allowlist |
| V3 Agentic supply-chain malware | Partial (deps scanned; skills GAP) | Crate pinning fixes versions; on top of it a cargo audit CVE gate and CycloneDX SBOM generation are wired in CI. Authoritative surface: the mono-repo root CI (irinityhq/irin .github/workflows/ci.yml) runs ONE workspace-wide cargo-audit + SBOM job over the shared Cargo.lock (pinned tool binaries, sha256-verified). Original-repo workflows are historical: gateway triad-ci.yml (audit + SBOM), sentinel ci.yml (audit + SBOM), council-rs ci.yml (audit; its SBOM is emitted in release.yml, not ci) — T23-R. Audit reads Cargo.lock against the live RustSec DB, so it runs even without a build. During the private phase the heavy CI jobs run on self-hosted runners; runner exposure is a named flip-gate item (fork-PR execution risk) before any public repo uses them. Upstream debt is tracked, not hidden: paste (RUSTSEC-2024-0436, unmaintained via fastembed/tokenizers) is a justified --ignore — see council-rs SECURITY.md + sentinel/docs/audits/2026-06-01-dependency-audit-playbook.md. Still GAP: installable skills execute unscanned in the operator context; no compose-not-rebuild watcher |
Deps leg shipped (E); skills supply-chain-watch + compose-not-rebuild remain (C/future) |
| V4 Sequence-level / multi-step | GAP | Single-step checks only (per-call budget, per-directive fence, per-fire dedupe); no stateful sequence watcher | B named → C0 assessment → D sequence-watch |
| V5 Human-agent trust exploitation | Partial — structural controls only | Arm is hardware-bound ceremony (finger on sensor, not a typed code), rate-limited, default-OFF; Escalation payload carries no LLM inference (deterministic evidence). GAP: no detector for persuasion in the Directive text itself — the defense is procedural + least-agency (recommend-only) |
Structurally bounded; graduates with D bounded execute. No new primitive |
| V6 Tool misuse / defend-from-own-agents | Partial → strong | Inbound multi-stage decontaminator + opt-in Sovereignty Gate endpoint (not default-wired outbound); execute locked (the action leg is not built, which is the most decisive mitigation); RED payloads / X-Sovereign-Mode force local-only routing |
D action allowlist + sandbox assessment (before any execute) |
| NSA MCP blind spot (cross-cutting) | GAP | The watch plane could observe the inverted MCP server→client channel legacy tools miss — nothing watches it today. council-rs runs an xmcp MCP client; no sentinel monitors it | B CISA/NSA standards map; C3 MCP adapter; D external-sentinel registration |
NIST AI RMF read-across (self-assessment, not conformance): the triad is
strongest on Govern (separation of powers across five primitives; dual-custody
arm; Council as decision authority; non-negotiable budget envelopes) and
Measure on the transaction layer (hash-chained audit, ledger/verify, JCS
canonicalization). It is weakest on Measure of the knowledge layer (V1) and
on Manage of sequences (V4). These are the same two gaps named above.
The posture reasons outward from a single declared default: the host is the perimeter.
Host boundary (primary). In the recommended docker-compose deployment
the Gateway is published only on 127.0.0.1:18080 (compose port mapping), not
exposed to LAN. Precision, because it matters: the nginx listener inside the
container is 0.0.0.0:8080 — loopback-only is a property of the compose
publish, not of the listener itself. Bare (non-compose) runs and the Council
port's token-dependent bind logic must be locked separately. The sidecar
management surface is a Unix socket (0o660, fail-closed), never proxied.
Client-supplied auth headers are replaced, never forwarded to upstream
providers. "No authenticated remote exposure" therefore holds under the
compose-publish + loopback assumption — not unconditionally.
Process boundaries inside the host.
| Boundary | Enforcement |
|---|---|
| Sidecar ↔ OpenResty | Unix Domain Socket (/run/sidecar/sidecar.sock), bypasses the network stack. Socket mode 0o660 (owner+group, world none); invalid mode refuses startup. This is transport isolation for the management surface, which nginx does not proxy. |
| Caller → Gateway | Virtual API keys (gw_live_…); token-bucket rate limiting global / per-IP / per-key; watch-plane auth enforced. |
| Escalation path | One locked route (model: council-triage through the Gateway's own router). No bespoke escalation REST endpoint — removes a class of trust-boundary bypass within the current in-process routing; the boundary assumes the Gateway itself (Lua router + sidecar) is not subverted — see §5. |
| Directive → outbox | Ed25519 signature + hash chain + directive_fence shape validation before signing. |
| Audit | Append-only; pruning/compaction cryptographically forbidden. |
| Producer arm | Dual custody; default-OFF; single-writer writer_claim enforced in code. |
The human-in-authority-chain arm gate. This boundary is designed to survive
a host compromise — with two caveats stated below, both real (see §5). Arming
the producer is dual-custody: a
host bearer token (custody domain 1, authorizes stage) plus a live
hardware ES256 attestation (custody domain 2, verified at confirm) — a Secure
Enclave / Touch ID key or a FIDO2 USB key. The second factor is a finger on a
sensor, not a copy/pasteable code; the private key never leaves the hardware. An
unexpected Touch ID prompt is treated as an alarm, not an inconvenience. The
asymmetry is deliberate: arming always blocks on the second factor; emergency
/disarm is single-principal fail-safe — any bearer in GW_ARM_PRINCIPALS
can kill the producer immediately, so a fast kill never waits on a second
signature (H-2 decided). Signatures carry a 30-day expiry; enclave/FIDO2
credentials are revoked by key de-enrollment, not token rotation.
Caveat 1 — the boot env-arm path (the bigger one): the boot path can arm
without the interactive ceremony (still audited as a boot_env_arm row,
still refused if the audit append fails). The EXECUTION_MODE=LIVE
circuit-breaker is shipped (gateway b985bc2): watch flags alone no
longer arm — the boot gate additionally requires EXECUTION_MODE to be
exactly LIVE plus the dispatcher key present. Config-presence is still
execution-authority in the reduced sense: for an operator who uses this
path, the hardware boundary reduces to filesystem-write (including the
explicit EXECUTION_MODE=LIVE line) + restart control. The breaker
narrows accidental arming (a copy-pasted .env without the LIVE line stays
dark); it does not stop a deliberate attacker with .env write.
Caveat 2 — the hardware factor's own limits: it raises the bar for a
remote attacker, but it lives on the operator's machine. Physical access,
firmware-level attacks, or an operator conditioned to tap through unexpected
prompts can defeat it, and both the enrollment tool (bin/arm-enroll) and the
enrolled-key registry (GW_ARM_ATTEST_KEYS_PATH) sit on the same host they
gate.
Provider APIs as untrusted inputs. Upstream model providers are outside the
trust boundary. Inbound prompts pass a multi-stage decontaminator
(oversized-payload, encoding/Base64, ghost-gate, malformed-content, zero-width,
homoglyph, and prompt-injection stages); a Sovereignty Gate alignment check
(/guard/sovereignty) exists as an opt-in endpoint callers may invoke — it is
not wired into the default outbound path.
Provider egress is SSRF-guarded by pin-on-resolve (resolve + explicit IP deny +
fail-closed on error/empty). RED-sensitivity payloads / X-Sovereign-Mode force
local-only routing, bounding exfiltration-via-provider to the local host. A
provider that returns manipulative or poisoned content is an expected adversary
here — but note V5's honest gap: nothing inspects the Directive text for
persuasion aimed at the human confirm.
MCP surfaces. council-rs runs an xmcp MCP client today. The NSA blind-spot warning applies: MCP inverts client-server so a server can query the client, over a channel perimeter tools don't inspect. The watch plane could observe that channel — but no sentinel watches MCP server→client traffic today. Treat any MCP server the operator connects as an untrusted, under-monitored surface (V3 supply-chain × the blind spot) until Phase C3 / the external-sentinel registration API close it.
Stated so an adversarial reader cannot claim the document oversold.
- A fully compromised host. The triad is a control plane, not a sandbox boundary against root on the box. See §5 for the honest enumeration. Everything in the localhost-only posture assumes the host itself is trusted.
- Same-uid local malware — the realistic case. Root is not required. Any
code running as the operator's uid (phished binary, browser RCE, a malicious
installable skill executing in the operator context — V3, unscanned today)
can read the on-disk signing materials, provider keys in
~/.zshrc, and.env; edit.envfor the boot-arm path; drop files forFileInboxSentinel; and poison stores watched only by the V1 gap. The Lua routing/decontaminator layer and thebin/arm*scripts are additional trusted code resident on the host. Most of §5 applies at same-uid, not just root. - A malicious operator. The Sovereign holds the host bearer and the hardware second factor. Dual custody is one human with two custody domains at n=1 — it raises the bar against a remote attacker who lacks the hardware factor, not against the legitimate operator choosing to arm and act.
- Provider-side compromise. A compromised or malicious upstream model provider is treated as untrusted input (guards in §3), but the triad does not defend the provider's own infrastructure, nor guarantee provider outputs are benign — only that acting on them stays gated behind deliberation + human arm + (deliberately unbuilt) execution.
- Multi-tenant isolation. v1 is single-tenant. The tenant field exists in the wire (APIs, comms envelope, DB) and is not stripped, but there are no cross-tenant isolation tests, no per-tenant quarantine, and no isolation proof — it is a v2 concern by locked decision. Do not run this as shared multi-tenant infrastructure.
- CI runner exposure. Heavy CI jobs run on self-hosted runners. Fork-PR workflows do not execute automatically: repository policy requires maintainer approval of workflow runs for all external contributors, so external code never runs unreviewed on operator hardware. A hosted or ephemeral runner lane for fork PRs is a named fast-follow.
- Multi-node / HA. Single-node by decision. There is no failover host and no high-availability boundary. Loss of the host is loss of the loop (fail-safe: it cannot fail into an armed state — see §5).
- Autonomous execution. Worker
executeis not shipped in v0.1. The signeddirective_outboxrow is the endpoint. Any sandbox / per-tool allowlist is a Phase D gate that must exist before execution is ever enabled. - Compliance / certification. No ISO/IEC 42001 certification, no NIST conformance, no CISA/NSA validation, no EU AI Act compliance claim, no SOC/SIEM posture. The standards mappings elsewhere in the repo are self-assessment for credibility, explicitly not conformance statements.
- Critical-infrastructure-grade assurance. Explicitly out of scope.
The one-line summary: the triad bounds what an agent can do; it does not bound what an attacker on the host can do. That is the local-first design trade, and it is why "localhost only, experimental, no compliance claims" stays until a later phase says otherwise with receipts.
Note on privilege: most of this section applies at the operator's uid — root
is not required. Signing materials are 0600 under the operator's home,
provider keys live in ~/.zshrc, .env is operator-writable, and Docker
volume access from the operator account typically bypasses in-container
permissions (including the UDS mode). Root adds completeness, not most of the
capability.
With the operator's uid (or root) on the box, an attacker can:
-
Read the on-disk signing keys and forge signed rows — per surface. Two separate materials: the Ed25519 ledger seed (
~/.irin/ledger_key.pem) signsaudit_events/ledger; the sidecar directive identity key (directive_identity.json) signsdirective_outboxenvelopes. An attacker with host read access to a given key can produce rows on that surface that verify cleanly — forging a verifiable outbox row requires the directive identity material, not (only) the ledger seed. The hash chain proves tamper-evidence over the recorded sequence, not that the signer was authorized at signing time. Signature validity is not the same as legitimacy once the key is on the same host as the attacker. -
Read every other credential the loop touches. Upstream provider keys in
~/.zshrc, virtual gateway keys, the host arm bearer, and.envcontents are all readable at the operator's uid. The Lua router/decontaminator and thebin/arm*tooling are modifiable trusted code at the same privilege. -
Tamper is detected after the fact, not prevented. The append-only, hash-chained audit makes any edit to recorded history detectable on the next
ledger/verify. It does not prevent an attacker from appending attacker-chosen entries, nor does it un-ring a bell — detection is retrospective. Off-host export/verification of the chain is what turns "detectable" into "detected." -
Poison the precedent / memory store undetected. Nothing watches the precedent corpus or a vector store for drift today (V1 GAP). A host attacker can slow-drip biased rulings into every future deliberation until
memory-integrity-watch(C1) ships and a store is named. -
NOT complete an interactive arm without the hardware second factor — with the boot-path caveat first. If the operator uses the boot env-arm path, this boundary reduces to
.env-write (including the explicitEXECUTION_MODE=LIVEline — the shipped circuit-breaker means watch flags alone stay dark) + restart: an attacker with deliberate filesystem write can still arm. On the interactive path the boundary holds: even with the host bearer (GW_ARM_PRINCIPALS) and full filesystem read, an attacker cannot produce the live Secure-Enclave/Touch-ID or FIDO2 signature required at confirm — that key material does not leave the hardware. A remote compromise can forge signed artifacts but not pass the interactive gate that turns the loop live (hardware-factor limits: §3 caveat 2). -
NOT auto-execute a destructive action through the triad. Worker
executeis not built. The most decisive tool-misuse mitigation is that the action leg does not exist — an attacker cannot fire a destructive tool through the loop today (they can, of course, run arbitrary commands directly as root; the point is the triad grants them no additional execution path). -
Deny availability, but not silently strand an armed producer. A host loss destroys the enclave key (no portable copy — that is the security property) and takes the loop down; recovery requires the off-host host bearer and re-enrollment on a new host. Fail-safe holds:
/disarmstays reachable on any surviving sidecar via the host bearer, and every safety invariant executes inside a DB transaction, so loss of the database can never fail into an armed or spending state.
What the host compromise does not get for free: the hardware confirm factor, a
second isolated node (there is none), and — because execute is unbuilt — an
autonomous action path. What it does get: read of on-disk signing keys, the
ability to forge verifiable signed artifacts, and undetected memory-store
poisoning. An operator protecting against a host-level adversary must keep the
signing key and the host bearer off the exposed host and export the audit chain
for off-host verification.
Key custody & recovery — GAP, named plainly. There is no documented
off-host backup, split-knowledge, or dual-custody ceremony for the two Ed25519
seeds today: they are 0600 files with no export procedure. Host loss means
re-key and re-pin (the T34 re-sign ceremony is the established pattern); the
arm-attest key is deliberately non-portable (enclave-bound — that is its
security property, not a gap). A documented custody/recovery procedure for the
two on-disk seeds is a named roadmap item, not a shipped control.
Please do not report security vulnerabilities through public GitHub issues.
Report privately via GitHub's private security advisory on the repository ("Report a vulnerability" under the Security tab), or by email to soc@irinity.com.
Please include:
- A description of the vulnerability and its impact.
- Steps to reproduce.
- Any relevant logs,
ledger/verifyoutput,ProviderResponsetrace, or code snippets.
Coordinated disclosure is expected: give the team a reasonable window to remediate
before any public write-up. Supported version: main (v0.1.0-rc series); a
supported-versions table lands with the first tagged public release.
Watch is cheap. Thought is rare. Action is final. Each tier earns the next.