Skip to content

Raucle suite threat model, v1 (STRIDE)

Status: reviewed — P4.C, 6 November 2026. This is the suite’s own trust boundaries under the scrutiny it applies to customers: every boundary gets assets, STRIDE threats, the control that genuinely exists in the merged code (named module and test, not aspiration), and the honest residual risk with a named mitigation. Where a risk is real and unmitigated, we say so rather than hiding it behind an existing test.

Scope: the merged architecture as of main at v0.24-era —

  • raucle (OSS core): capability gate (raucle/capability.py, raucle/gateway.py), policy engine + data-flow rules (raucle/policy.py), receipts & offline verifier (raucle/provenance.py, raucle/verify.py), receipt index (raucle/receipt_index.py), URL-switch gate proxy (raucle/urlgate.py), transparency/SCITT anchoring (raucle/transparency.py, spec docs/spec/transparency-v1.md), bilateral executor co-signatures (raucle/bilateral.py), pulled audit-source connectors (raucle/connectors/), containment CLI (raucle/containment.py, raucle/cli.py containment hold|resume|status), KMS signer abstraction (raucle/kms.py).
  • raucle-cloud (private): portal admin surface (src/raucle_cloud/portal/), org-scoped store with the durable ingest queue (portal/store.py), bounded projection (portal/projection.py), offline verification (portal/verify.py).
  • raucle-agent-red: fuzz corpus, baselines, environment ledgers — the adversarial tooling, itself a trust surface (it can generate what looks like evidence).
  • open-agent-assurance: assurance packs, role profiles, console.

Out of scope for v1: the IONOS/Caddy deployment plane (covered operationally) and the Lean mechanisation’s model vs implementation gap (documented in docs/security-model.md and not re-litigated here).

Method: STRIDE per boundary, plus a residual-risk register at the end prioritised for future waves.


T1 — Issuing-side trust boundary: operator/gateway minting keys

Section titled “T1 — Issuing-side trust boundary: operator/gateway minting keys”

Assets. The root issuing key(s) that sign every capability token, receipt and policy. Everything the suite attests downstream of mint time inherits this key’s integrity.

Existing control. Signer abstraction with KMS/HSM backends (raucle/kms.py: KMSSigner, AWSSigner, AzureSigner, VaultSigner) so the private key need not live as disk PEM in production; issuer versioning for rotation via the dual-trust runbook (docs/security/key-operations.md), with the trust registry’s one-active-key-per-issuer-name rule acting as the impersonation control (a second classical key under the same issuer name is refused; rotation moves to a versioned identity such as acme.bank/v2); mixed-generation chains verify whole with all generations’ public keys. Deployment guidance keeps prod material in untracked overlays.

Threats.

  • Spoofing/Tampering: a compromised gateway is the issuer — it can mint arbitrary capabilities with fake constraints, proofs and attenuation, all correctly signed. Receipts and tokens offer no defence against the root keyholder being adversarial: the independent validation round-1 research (research-2026-11-05-perplexity-validation-round1.md) names this explicitly — “no defence if the root key or operator is adversarial”.
  • Repudiation: operator denies minting; receipts bind the gate identity (agent:gate JWS signer), so runtime repudiation is covered, but mint-time repudiation is only as strong as key custody.
  • Info disclosure: local PEM default on dev/small deploys.

Residual risks (honest).

  1. Single-rooted trust. The default and common deployment concentrates mint authority in one operator key that can mint every capability and every receipt. This is a vulnerability class, not a hypothetical: we named it ourselves in research and repeat it here. Nothing in the current code makes minting multiparty. Mitigation needed (named module): split issuance across roles in raucle/approvals.py + a raucle/mint quorum gate (M-of-N cosign at mint time), with the receipt chain carrying the quorum — see mitigation M1.
  2. KMS/HSM integration exists but is opt-in by env var; a wrong env var in the deploy overlay silently runs local PEM (the silent-defaults failure mode the repo has shipped before). Mitigation needed: a startup assertion + test pinning that a production profile with RAUCLE_SIGNER unset fails loudly. Lands in raucle/kms.py create_signer (M2).

T2 — Evidence boundary: receipt chain → verifier → SCITT anchor

Section titled “T2 — Evidence boundary: receipt chain → verifier → SCITT anchor”

Assets. The append-only receipt chain and its checkpoint; the offline verifier’s “invalid when it is invalid” verdict; the transparency anchor.

Existing control. Hash chain + Merkle root + signed checkpoint (raucle/audit.py); truncation of the tail only detectable with an external high-water mark — the SCITT anchor (raucle/transparency.py, spec docs/spec/transparency-v1.md) registers the chain head so the writer cannot later present a different history for the same chain. Verification is fail-closed both in the core verifier (raucle/provenance.py: ProvenanceVerifier, size caps, strict alg=EdDSA/crit enforcement, no alg substitution), the harness finding contract (claim now, verify later, ghost citations fail named), and the portal (portal/verify.py — a failed chain renders as invalid, first line, non-zero exit — pinned in tests).

Threats.

  • Tampering/Repudiation: in-place edit, reorder, splice within the signed prefix → detected. Signature stripping → invalid (downgrade).
  • Info disclosure: receipt reasons can quote constraint values; exports already practice bounded disclosure (ids/digests/decisions only; the portal projection deliberately has no reason column).

Residual risks (honest). 3. Receipt-hash anchoring limits. An anchor proves non-equivocation of the head at registration time and nothing more: it cannot catch truncation of records appended after the register (you must anchor frequently or on a schedule), it does not attest the truth of any decision, it does not upgrade record quality, and per-receipt hashes (receipt_hash in the index and portal) are derived values — aggregate checks over recorded hashes prove nothing about payloads unless each hash is recomputed from its own payload (a rule already encoded in our escrow re-checks). If the Transparency Service itself is a single vendor, its availability and honesty become a new boundary. Mitigation needed: an automatic anchor cadence + an anchor-lag alarm, and a dual-notarisation option; lands in raucle/transparency.py + the portal deploy profile (M3). 4. Derived projections: the OSS receipt index INSERT OR REPLACEs on receipt_hash as PK — byte-identical same-second attempts collapse and trace queries undercount. Known, documented, and the repair contract (rebuild_from_store) exists; residual risk is that operators who never rebuild undercount. Mitigation needed: switch to plain INSERT + rowid tiebreak (already specified in maintenance notes), landing in raucle/receipt_index.py (M4).

T3 — Enforcement boundary: policy engine and approvals

Section titled “T3 — Enforcement boundary: policy engine and approvals”

Assets. The policy file (deny-allowlist semantics), approval thresholds, and the new rule kinds (data-flow/chain-taint destination-class rules, URL switch modes, EQR, ePAeC verifier rules).

Existing control. Fail-closed construction throughout: data-flow rules deny when taint meets an unallow-listed destination class and deny on an unknown destination class even in evaluation mode (raucle/policy.py, tests/test_policy.py); policy load noise is isolated; approvals are receipted decisions (raucle/approvals.py); every rule kind ships with tests exercising the real gate surface, not internals.

Threats.

  • Tampering: the policy file is a mutable control-plane artefact. Swapping it (or revocations JSONL) silently changes global enforcement; no signature is required on policy content today. This is not encrypted or signed in the merged code. Containment writes policy too, so an attacker who can write the policy file can both loosen and lock enforcement.
  • Elevation/spoofing: approvals queue manipulation from a co-resident process.
  • DoS: pathological rule patterns are bounded by construction, but a hostile policy author (the deployer’s own repo) is not an adversary we defend against — policy files are trusted configuration by design.

Residual risks (honest). 5. Unsigned policy/revocations/containment state. Filesystem write access to the enforcement directory equals full control-plane compromise; there is no detection-of-tamper on policy bytes today. Mitigation needed: hash-and-sign policy/delegation files at load and reject divergence vs a seal — landing in raucle/policy.py (seal sidecar per the existing checkpoint pattern, M5).

T4 — Adoption boundary: the URL-switch gate proxy (passthrough)

Section titled “T4 — Adoption boundary: the URL-switch gate proxy (passthrough)”

Assets. raucle/urlgate.py — config-only adoption: stock SDKs pointed at the proxy; minted per-call receipts; deny path with upstream-shaped errors.

Existing control. The gate consults the real RaucleGateway policy engine server-side; denies never reach the upstream; receipts are minted for both allow and deny; loopback-only bind by default (--i-know-this-is-public override); hop-header stripping and origin-form checks on the proxy path (tests/test_urlgate.py).

Threats / honest residuals.

  • The passthrough trust assumption. Non-gated traffic (GET /v1/models, non-chat endpoints) passes through untouched, and gated-call response bodies are forwarded verbatim: the proxy attests that policy was consulted and a receipt minted — it does not verify the upstream’s response content, does not inspect streamed SSE bytes (re-framed only at buffer boundaries), and the “gated” set is the path→tool mapping (_gate_tool_for_path). A malicious upstream, or an adversarial response, flows to the client with an allow receipt that says nothing about it. This is a documented limitation in the module docstring — kept here as a named residual, not explained away.
  • Spoofing: nothing binds a receipt to response correctness; a downgrade client could point at a different upstream and still mint receipts for calls the gate cannot see (receipt attests gate consultation, not upstream identity). Mitigation candidate M6: pin the upstream hostport in the receipt’s x_gate payload so a verifier can detect proxy/upstream drift, landing in raucle/urlgate.py + raucle/provenance.py.
  • Repudiation/evasion: response bodies carry no attestation; an end-to-end output attestation (hash the forwarded body into the receipt) is scoped out for v1 because streamed responses cannot be hashed before delivery — the honest position is that output-side assurance stops at the passthrough wall until a post-hoc digest can be appended.

T5 — Containment (P4.A): the halt is now both an asset and an attack surface

Section titled “T5 — Containment (P4.A): the halt is now both an asset and an attack surface”

Assets. The containment CLI itself: global (deny-all policy swap), agent (revoke delegated subtree), chain (revoke one token subtree); the containment receipts stream; the operator keypair at ~/.raucle/containment/.

Existing control. Every transition is a signed, auditable, reversible receipt — “a halt without its receipt is a bug, not a feature” (raucle/containment.py, tests/test_containment.py); fail-closed rollback if receipt emission fails; hold/resume serialise on flock over the target so concurrent transitions interleave nowhere; malformed scope, missing prerequisites and double-holds are named errors.

Threats.

  • Spoofing/Elevation (the new surface): whoever holds the operator key halts everything — the global hold swaps the live policy for deny-all, zero rules, fail-closed on the normal load path. Two implications, both real: (a) theft — steal the containment key/harness and you can blackmail an agent fleet into lockout (a denial-of-service amplifier whose receipts will happily verify as legitimate); (b) malice/recklessness — a single operator can halt production with no counterparty check, no two-person rule (contrast M1’s mint quorum). The receipt chain proves who halted, but does not prevent it.
  • DoS: by design the CLI is the DoS. The global scope has no blast-radius config; it is all-or-nothing.
  • Repudiation: covered: signed JWS operator receipts, offline-verifiable with raucle verify-receipt.
  • Tampering: rollback/restore is SHA-256-verified both sides; but the underlying files (policy, revocations) stay unsigned (residual 5).

Residual risks (honest). 6. Unauthenticated operator authority. The operator key is generated once at first use, stored next to the state dir, and the CLI has no separate authentication, no quorum, and no scope beyond three levels. v0 is honestly scoped (“trace quarantine and lease suspension are v1”) and this power is more concentrated than minting (it needs only one key and takes effect immediately). Mitigation needed: M-of-N containment receipts (the paired resume pattern naturally extends to paired co-signs), an allowlist of actors, and alerting on containment_hold in the portal — landing in raucle/containment.py + portal ingest (M7).

T6 — Portal boundary (raucle-cloud): durable ingest queue and bounded rows

Section titled “T6 — Portal boundary (raucle-cloud): durable ingest queue and bounded rows”

Assets. The org-scoped store (portal/store.py), the ingest offset queue, bounded-disclosure rows, operator auth for the admin surface.

Existing control. The chain file remains the tamper-evident truth; the store is explicitly a disposable projection — only complete append-only lines are consumed (safe against a mid-append writer), the whole pass is one transaction so a crash lands rows and offset together or neither, and a shrunk chain file triggers a drop-and-reingest of that org’s rows. Bounded disclosure is enforced structurally: projection.py’s _bounded_row is the only path from store to route (no reason column at all, deny reasons quoting constraint values cannot leak). Pages are server-side clamped.

Threats / honest residuals. 7. Stop-the-world recovery. The lock (self._lock) is per-store, shared across orgs: when an org’s chain file shrinks (retention/destruction re-anchors the chain), that org’s rows are DELETEd and fully re-ingested while holding the shared lock — every other org’s sync stalls behind it, and re-ingest of a long chain is a single blocking pass. The design accepts this for v1 because correctness beats latency, but it is a documented scaling and availability risk: one org’s chain rewrite becomes every org’s ingest stall. Mitigation needed: per-org locks or an out-of-band rebuild task; landing in portal/store.py sync_org (M8). 8. Info disclosure: malformed/non-receipt lines are skipped silently (“the projection never rejects chain content”) — a mis-ingested or tampered-with line quietly does not accelerate. Honesty depends on the portal never being the source of truth; the moment operators trust rows without running raucle verify-chain (portal verify.py), silent skips become a divergence risk. Mitigation needed: a skip-counter column and a reconciliation job comparing projected hashes against the chain tail (M9). 9. DoS: the ingest reads the whole gap fh.read() between visits — an unbounded append burst grows the buffer per pass; bounded by typical volumes, unbounded by design. Mitigation: chunk the read per pass.

T7 — Connector boundary (pulled data: Entra, Okta, GitHub audit)

Section titled “T7 — Connector boundary (pulled data: Entra, Okta, GitHub audit)”

Assets. Vendor audit events entering the chain as evidence; the connector mappings (docs/spec/connectors/v1/*).

Existing control. Injected transport (production client / recorded fixtures); deterministic, spec-pinned field mappings (MAPPING_VERSION, canonical sha256 per event mapping, vector corpus against drift); rate limits raise RateLimitError carrying the vendor’s own Retry-After — never invented; cursor paging (opaque $skiptoken passed verbatim, never parsed) with session + at-cursor dedupe; attribution honest (“unknown, reason…” — no invented identities) (tests/test_connectors_*.py).

Residual risks (honest). 10. Vendor-origin trust. Connector events are signed by nothing from the vendor: they enter the chain with a chain hash but zero cryptographic binding to the source — the suite attests “we pulled this from an HTTPS endpoint and mapped it deterministically”, not “Entra asserts this”. Dedupe is session/cursor-local; append-twice across processes pollutes correlation. Mitigation needed: persist next-cursor + source ids in the connector state (documented as caller duty today; make it first-class), and record the upstream URL + fetch timestamp in each chain event (raucle/connectors/base.py + HashChainSink integration, M10).

T8 — Bilateral outcomes and the executors’ co-signature

Section titled “T8 — Bilateral outcomes and the executors’ co-signature”

Assets. x_bilateral executor co-signatures over the closed outcome vocabulary (executed|failed|rejected), receipt output_hash.

Existing control. Closed vocabulary enforced at emission and re-checked at verification (a non-vocabulary outcome is a named verification failure); the co-signature binds outcome + output hash + executor key id over canonical JSON (raucle/bilateral.py, tests/test_x_bilateral.py).

Residual risks (honest). 11. The co-signature proves the executor asserted the outcome against that output hash — it cannot prove the executor evaluated the same bytes the executor received if the channel to it is compromised; and an executor key is a second key class whose compromise yields confidently-wrong attested outcomes. Mitigation: executor keys through the same KMS boundary and the same versioned-identity rotation as T1 (reuse M2’s startup assertion for executor signers).

T9 — Red tooling and assurance console (evidence about evidence)

Section titled “T9 — Red tooling and assurance console (evidence about evidence)”

Assets. raucle-agent-red’s fuzz corpus and baselines; open-agent-assurance packs, role profiles and console; the docs-alignment checker (scripts/check_docs_alignment.py, fence-aware).

Existing control. Findings split claim from truth by design (x_finding: emission unchecked, verification enforces the cited receipt exists and its verdict matches); exported artefacts never upgrade bad evidence (failed chains render as failed, first line, non-zero exit); the checker fails visibly on malformed markdown so assurance docs can be silently half-updated.

Residual risks (honest). 12. Red tooling can generate plausible-looking baselines by construction — that is its job — so anything an assessor reads from red output must be reproducible from a pinned corpus commit; the residual is process risk (assessor trust in commit pinning), mitigated by the baseline manifests, not by code. Docs-alignment currently exists only in open-agent-assurance; the OSS core has no equivalent checker (see M11).


Mitigation register (prioritised for future waves)

Section titled “Mitigation register (prioritised for future waves)”

Each item names the module it lands in; priority by (exploitability × blast radius), not by effort.

# Risk ref Mitigation Lands in Priority
M1 R1 Mint-time quorum: M-of-N co-signed capability issuance, quorum recorded in the receipt chain raucle/capability.py + raucle/approvals.py; schema in raucle/provenance.py P0 — removes the single-root-key vulnerability
M2 R1, R11 Production signer assertion: refuse to start a prod profile without an explicit remote-signer config; add from_env round-trip test raucle/kms.py P0 — silent-defaults class
M5 R5 Seal-on-load for policy/revocations/delegations: signed sidecar rejected on divergence, per the checkpoint pattern raucle/policy.py (load path), containment state via raucle/containment.py P0 — filesystem write = full control-plane takeover today
M7 R6 Containment authority hardening: co-signed holds (extend the paired resume pattern), operator allowlist, portal alert on containment_hold raucle/containment.py + portal/store.py ingest + portal/detail.py P1 — the halt is an unquorate kill switch
M3 R3 Anchor cadence automation + anchor-lag alarm; dual-notarisation option raucle/transparency.py, prod deploy profile P1
M8 R7 Stop-the-world fix: per-org locks or out-of-band rebuild in sync_org portal/store.py P1 — multi-tenant availability
M6 T4 Pin upstream hostport in receipts so verifier detects proxy/upstream drift raucle/urlgate.py + raucle/provenance.py (+ schema vectors, strict-verifier-first) P2
M9 R8 Skip-counter column + reconciliation job (projected hashes vs chain tail) portal/store.py / portal/projection.py P2
M10 R10 First-class connector state: persisted cursor + source ids; upstream URL + fetch timestamp in each chain event raucle/connectors/base.py, HashChainSink wiring P2
M4 R4 Receipt index: plain INSERT + rowid tiebreak to stop byte-identical-attempt collapse raucle/receipt_index.py P2 (undercount is observable)
M11 R12 Port the fence-aware docs-alignment checker to the OSS repo and run it in CI on docs/ scripts/check_docs_alignment.py (adapt) + workflow P3
M12 T4 Output-side attestation scoping note: document explicitly that streamed SSE responses carry no output digest, and design the post-hoc digest shape before promising it docs/security-model.md + raucle/urlgate.py P3

U2 residual register (recorded 8 November 2026)

Section titled “U2 residual register (recorded 8 November 2026)”

Four residual risks carried through the U2 backlog had no home in the STRIDE sections above and lived only as code comments or scattered doc notes. They are recorded here in the same honest register format (threat, exposure, decision or limit, owner, date). Owner for all four is the OSS maintainers (Epic28); each decision is dated.

R13 — TOTP secret stored in plaintext beside hashed API keys (RED-032)

Section titled “R13 — TOTP secret stored in plaintext beside hashed API keys (RED-032)”
  • Threat: Info disclosure. A file-system read of users.jsonl (or any backup, snapshot or log volume holding it) yields a usable TOTP seed for each admin account.
  • Exposure: Local attacker with read access to the data dir, or anyone holding a backup of it. The key material itself is safe: only the SHA-256 digest of each API key is stored (RED-032, raucle/gateway.py), and keys are looked up by digest with no plaintext on disk. The TOTP secret is the plaintext exception.
  • Decision/limit: Known residual, deliberately not fixed in the RED-032 round. Protecting the secret needs a master-key source (encrypt the secret at rest, unlock at boot), which is a deployment decision, not a change to the key-digest scheme. Also recorded in the API Key Management section of docs/gateway-security.md.
  • Owner: Epic28 OSS maintainers.
  • Date: recorded 8 November 2026.

R14 — Gate authentication default off is an explicit owner decision

Section titled “R14 — Gate authentication default off is an explicit owner decision”
  • Threat: Spoofing. With RAUCLE_GATE_AUTH=off, the /gate endpoint trusts the declared agent_id; any caller able to reach the port can attribute decisions to an arbitrary agent.
  • Exposure: Every caller of POST /gate when the deployment has not changed the default. This is a posture risk confined to deployments that sit outside a trusted internal network.
  • Decision/limit: The owner decided on 8 November 2026 to keep off as the shipped default, for the demo and backwards compatibility. The shipped mitigations, recorded so the default is not mistaken for an oversight: the boot banner warns loudly (GATE AUTHENTICATION IS OFF, raucle/gateway.py); GET/PUT /api/config exposes gate_auth so an operator can move to apikey or token without a redeploy; and the production-hardening checklist requires the mode not be off for anything internet-adjacent (docs/getting-started/20-production-hardening.md).
  • Owner: Epic28 OSS maintainers (owner decision).
  • Date: 8 November 2026.

R15 — SIEM target validation does not resolve DNS (RED-036)

Section titled “R15 — SIEM target validation does not resolve DNS (RED-036)”
  • Threat: Elevation by proxy. siem_target_allowed (raucle/gateway.py) rejects non-HTTP(S) destinations and literal loopback, link-local and private addresses, but a hostname that resolves to a private address passes validation and the forwarder then posts wherever DNS points.
  • Exposure: An admin-panel operator (or an attacker who has compromised an admin session) can turn the forwarder into an outbound-request primitive aimed at internal infrastructure by naming a re-resolving or private-resolving hostname.
  • Decision/limit: Known residual, documented by the validation function itself and cross-referenced from the SIEM Integration section of docs/gateway-security.md. Resolving DNS at config-write time is the candidate fix; until then, deployment guidance is to keep the gateway’s egress paths behind an allow-listing egress control. RAUCLE_SIEM_ALLOW_PRIVATE=1 remains the deliberate opt-in for internal collectors.
  • Owner: Epic28 OSS maintainers.
  • Date: recorded 8 November 2026.

R16 — _FileCache same-size edit staleness

Section titled “R16 — _FileCache same-size edit staleness”
  • Threat: Tampering with stale reads. _FileCache (raucle/gateway.py) keys entries on (mtime_ns, size). An edit that changes a registry file’s contents without changing either — an in-place rewrite of the same byte length — can be served stale until the next genuine invalidation.
  • Exposure: The registries cached this way are the revocation list, trace quarantine and lease suspension registries, trust registry and delegation ledger. A co-resident process able to rewrite a registry file in place could keep a revocation from landing on the next call.
  • Decision/limit: Known residual, accepted because the registries are operator-managed append-only files: any normal append changes size and therefore invalidates, so the risk requires a hostile in-place rewrite of an operator file — the same filesystem-write-trust assumption the unsigned-policy residual (R5 above) already covers. Do not treat this and R5 as separate work items: a raucle/policy.py seal-on-load control (M5) closes both, and any fix for this entry must cross-reference R5 in docs/gateway-security.md’s cache notes to avoid double-working.
  • Owner: Epic28 OSS maintainers.
  • Date: recorded 8 November 2026.

No threat model is a control; the table above is a plan, and P0/M1 means nothing until it ships. The strongest existing evidence for the suite’s honesty is that this model and the residual risks inside it live in the same public repo as the code they critique, and that every “existing control” line cites a module and test a reviewer may run rather than take on faith.