Raucle suite threat model, v1 (STRIDE)
Raucle suite threat model, v1 (STRIDE)
Section titled “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, specdocs/spec/transparency-v1.md), bilateral executor co-signatures (raucle/bilateral.py), pulled audit-source connectors (raucle/connectors/), containment CLI (raucle/containment.py,raucle/cli.pycontainment 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:gateJWS 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).
- 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+ araucle/mintquorum gate (M-of-N cosign at mint time), with the receipt chain carrying the quorum — see mitigation M1. - 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_SIGNERunset fails loudly. Lands inraucle/kms.pycreate_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_gatepayload so a verifier can detect proxy/upstream drift, landing inraucle/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
globalscope 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/gateendpoint trusts the declaredagent_id; any caller able to reach the port can attribute decisions to an arbitrary agent. - Exposure: Every caller of
POST /gatewhen 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
offas 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/configexposesgate_authso an operator can move toapikeyortokenwithout a redeploy; and the production-hardening checklist requires the mode not beofffor 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=1remains 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.pyseal-on-load control (M5) closes both, and any fix for this entry must cross-reference R5 indocs/gateway-security.md’s cache notes to avoid double-working. - Owner: Epic28 OSS maintainers.
- Date: recorded 8 November 2026.
What this document does not claim
Section titled “What this document does not claim”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.