← Trust / Threat model

Threat model — the honest version.

v0.2 · 2026-08-03 · scoped to the Phase C egress broker

This is the un-marketing-cleaned version an external security reviewer will read. Every named gap has either a compensating control or a shipped roadmap reference. Nothing is hidden. The canonical markdown source lives in the station repo — this page reflows it for the web; when the two disagree, the source wins.

1. Executive summary

RailCall Station is a Layer-2 governance server that runs locally on the operator’s machine, binds 127.0.0.1:8799, and sits inside the workflow — between the LLM/agent and the destination API. Every model call goes through station.llm.complete(), and every call emits a signed egress receipt into the same hash-chained audit log as action receipts.

The receipt is metadata only — no prompt content, no response body, no user identifiers — but records which module made what call to which provider, when, under which policy version. Two frameworks (SOC 2 authorization + HIPAA §164.312(b) audit), one artifact, one verification path.

2. Data flow diagram


┌──────────────────────────────────────────────────────────────────┐
│  OPERATOR'S MACHINE                                              │
│                                                                  │
│  ┌────────────┐   ┌───────────────┐   ┌─────────────────────┐    │
│  │ VS Code    │   │  Studio HTTP  │   │  MCP Server         │    │
│  │ Extension  │──▶│  127.0.0.1:8799│──│  (stdio to Claude  │    │
│  │            │   │               │◀──│   Desktop / Cursor) │    │
│  └────────────┘   └───────┬───────┘   └──────────┬──────────┘    │
│                           │                       │              │
│                           ▼                       ▼              │
│                  ┌──────────────────────────────────────┐        │
│                  │  Airlock + Egress Broker (station)   │        │
│                  │  · signed approval receipts          │        │
│                  │  · signed egress receipts            │        │
│                  │  · hash-linked audit chain           │        │
│                  └──────────┬─────────────┬─────────────┘        │
│                             │             │                      │
│                             │             │  station.llm.complete│
│                             ▼             ▼                      │
│                   ┌────────────┐   ┌──────────────┐              │
│                   │ Cooperative│   │ Provider SDKs│              │
│                   │  Modules   │   │ (Anthropic,  │              │
│                   │  (signed)  │   │  OpenAI, …)  │              │
│                   └────┬───────┘   └──────┬───────┘              │
│                        │                  │                      │
│                        │  (adversarial    │                      │
│                        │   module bypass  │                      │
│                        │   via ctypes /   │                      │
│                        │   raw socket)    │                      │
│                        │  ─ ─ ─ ─ ─ ─ ─  │                      │
│                        ▼                  ▼                      │
├──────────────────────────────────────────────────────────────────┤
│                    NETWORK BOUNDARY                              │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│    ┌──────────────┐   ┌──────────────┐   ┌──────────────┐        │
│    │ LLM Provider │   │ Destination  │   │ Marketplace  │        │
│    │ (OpenAI, …)  │   │ APIs (Slack, │   │ Backend      │        │
│    │              │   │  Discord, …) │   │              │        │
│    └──────────────┘   └──────────────┘   └──────────────┘        │
│                                                                  │
│                    RailCall servers touch NOTHING here —         │
│                    marketplace backend is the ONLY               │
│                    RailCall-operated egress, and it              │
│                    handles listings + license state,             │
│                    never prompt content or receipts.             │
└──────────────────────────────────────────────────────────────────┘

Key structural facts: everything the operator considers sensitive stays on the operator’s machine. RailCall-operated servers never see prompts, responses, or receipts. The marketplace backend is the one RailCall-operated egress — it serves listings + license state; prompt content and receipts never touch it.

3. Assets to protect

AssetWhereSensitivity
BYOK provider API keysWS/keys.local.json (0600)HIGH
Named credentialsWS/credentials.local.json (0600)HIGH
Ed25519 signing seedOS keychain OR WS/*.json (0600)CRITICAL — install identity
Session tokenWS/session_token.json (0600)HIGH — grants API access
APPROVE token (2nd credential)WS/approve_token.json (0600)HIGH — dual-control
Audit chainWS/audit_log.jsonl (append-only)INTEGRITY-CRITICAL
Action receiptsWS/receipts/**INTEGRITY-CRITICAL
Egress receiptsWS/receipts/egress/INTEGRITY-CRITICAL
Policy docWS/policy.jsonINTEGRITY-CRITICAL
Prompt content (in-flight)Python process memory (never persisted)HIGH — out of scope after call
Provider response bodyPython process memory (never persisted)HIGH

4. LINDDUN — privacy threats

LLinkability

payload_hmac_hex is deterministic per (install, prompt). Two identical prompts on the same install → identical HMAC — that IS information (they know the same prompt was sent twice). Cross-install correlation is broken by install-bound HMAC key. Residual: metadata timing + shape correlate to identity if an observer has enough context.

IIdentifiability

Receipt has no user names, emails, session ids, or IPs. module_id identifies WHICH module, not WHO. caller_kind is coarse-grained channel only.

NNon-repudiation of the operator

By design. Ed25519 signature is verifiable off-box against the install pubkey. HIPAA §164.312(b) audit requirement satisfied. Applies to metadata of the call — not to prompt content (which is never persisted).

DDetectability

Every call is detectable + attributable. That's the point — SOC 2 auditors want this. An attacker who steals the audit log learns frequency + shape + provider, not content.

DDisclosure

Enforced by construction: receipt never contains prompt or response bytes; HMAC is one-way keyed. Residual (documented): module_version reveals version, endpoint_url_host is host-only precisely because URL paths carry tenant IDs, token_count_actual correlates weakly with response type.

UUnawareness

Every receipt is visible in Studio's Receipts tab and via /api/receipts/list. Operator can inspect any receipt on-disk. Third-party subjects (users of an application built on the station) are the application operator's responsibility to notify.

NNon-compliance

Metadata-only design keeps us out of PHI/PII scope by construction. We do NOT claim 'PHI-safe by construction' — that's overreach. Correct framing: HIPAA §164.312(b) audit requirement is satisfied; SOC 2 authorization + confidentiality are supported. The receipt is not a substitute for the operator's BAA with the model provider.

5. STRIDE — security threats

AttackMitigationResidual
Cross-origin browser POST (Spoofing)CSRF gate requires Origin/Referer; session token in same-origin fetch only.Localhost `null` origin blocked by explicit check.
Impersonate a signed module (Spoofing)Publisher Ed25519 signature verified at load via publisher_trust allowlist (v0.25 Phase 4a).Publisher key compromise → new module installs are refused; existing signed modules stay valid until unpinned.
Edit an on-disk receipt (Tampering)Ed25519 signature over canonical bytes; verify rejects any bit-flip. Test coverage: tests/test_egress_receipt.py.Deleting the entire receipt file goes through the audit_chain integrity check — but tail-truncation by the install-key holder needs an off-box witness (Phase D2).
Delete a receipt to hide it (Tampering)audit_chain.append writes a fingerprint entry; verify walks the chain and flags gaps as first_break.Tamper-evident for in-place edits + mid-chain deletion; tail truncation by the install-key holder needs off-box witness (Phase D2).
Edit the policy doc to fabricate an allow (Tampering)policy_version_hash embedded in the receipt at emit time; verifier compares to a fresh hash of the current doc.If policy_doc + receipt are altered together and re-signed with the install key, tamper is undetectable without off-box policy attestation (Phase D2).
Exhaust disk with runaway egress receipts (DoS)Every call = ~1 KB receipt; audit chain uses append-only + transactional writes.Per-module receipt-rate cap is not yet in-shim (v2 roadmap).
Cooperative module escapes to write outside WS (Elevation)C2.1/C2.2 OS sandbox refuses connect(2) except loopback; C2.0 CONNECT proxy filters destinations. Publisher trust is the compensating control for the same-process reads that remain.An adversarial module can still open() any 0600 file the station user can read — same-user attacker is out of scope (they own the machine).
Module reads BYOK credentials it wasn't declared for (Elevation)v0.43 credential-spec collision handler auto-namespaces per module.Same-user attacker limitation: any process running as the station user can open the credential file directly.

6. Cooperative vs adversarial — the honest scope

This is the single most important scoping call in the deckand it’s stated here up front so no reviewer feels ambushed by it.

A cooperative module imports station.llm.complete and calls it. Every call produces a signed receipt. The receipt IS the proof of what happened. For cooperative modules, the guarantee is real.

An adversarial module could open a raw socket via ctypes and bypass the broker. On macOS (v0.46+), the sandbox-exec profile makes the kernel refuse connect(2) except to the loopback broker port. On Linux (v0.46+), systemd-run --scope with IPAddressDeny=any installs a BPF cgroup filter with the same effect. Modules spawned outside those wrappers still fall under the cooperative + publisher-trust framing.

Compensating controls that make cooperative-scope acceptable: every module ships with an Ed25519 publisher signature verified at load; publisher trust is explicit-opt-in (allowlist); marketplace-published modules pass a human pre-publish review. Auditors accept named gaps with compensating controls; they reject undisclosed gaps or claims of coverage that doesn’t exist.

7. Roadmap — what closes each named gap

shippedAdversarial modules egress without a receipt

C2.0: env-var CONNECT proxy (env-respecting clients caught). C2.1 (macOS): sandbox-exec kernel-refuses connect(2) except loopback broker. C2.2 (Linux): systemd-run --scope + IPAddressDeny (BPF cgroup filter). Raw-socket / ctypes bypass closed on macOS and modern-systemd Linux.

shippedSelf-asserted receipt timestamp

C1b: RFC 3161 anchor via freetsa.org, embedded on every receipt.

shippedInstall-time contract for what a module will call

C1c: `allowed_destinations` in module.json + load-time verification; violations emit a signed refusal receipt.

shippedPrompt classification (recall on real PHI)

C3.0: pattern-count monitor. C3.1: `redacted` decisions actually scrub the forwarded prompt. C3.4: optional Presidio ML NER adapter — soft dep, higher-recall on US_SSN, IP_ADDRESS, IBAN_CODE, MEDICAL_LICENSE, etc. Regex fallback when Presidio absent.

shippedPolicy engine for the `decision` field

C3.0/C3.1: flat-atom rules → enforcement. C3.2: `approved` blocks in-process for human decision (fail-closed timeout → deny). C3.3: compound conditions (all/any/not), named rules, bundle files. OPA/Cedar as external subprocess remains optional future work — compound engine covers the common cases.

shippedTokenization (round-trip identifier stripping)

C4: reversible `[TOK_<cat>_<hash8>]` tokens replace the one-way `[REDACTED:*]` placeholder. Provider sees tokens, per-workspace map lets the response detokenize before it reaches the workflow user. Deterministic HMAC per install.

shippedTrust-tier routing (local → BAA → frontier)

C5: rules can declare `then: rerouted` with a `route_to: {provider, model}` target. Receipt records BOTH the caller-requested destination AND the actual (rerouted) one. `destination.trust_tier` (local / baa_covered / frontier / unknown) stamped on every receipt.

shippedOff-box audit chain witness (defeat tail-truncation)

D2: stations POST signed `railcall_witness_anchor.v1` anchors — chain-head sha256 + policy hash + install pubkey + station wall clock — to POST /witness/anchor on the marketplace backend. Manual trigger via POST /api/witness/anchor; background timer opt-in via RAILCALL_WITNESS_URL + RAILCALL_WITNESS_AUTO=1 (default OFF — the station stays byte-identical to pre-D2 for operators who don't opt in). Auditors GET /witness/anchors?pubkey=... to fetch the anchor history and compare against the station's live chain head; mismatch = evidence someone rewrote history between anchors.

shippedOff-box policy doc attestation

D2: same anchor carries policy_hash (sha256 of policy.json + egress_policy.json) + policy_id so an auditor can prove a specific policy doc was in force at a specific witness-side receive time. Auditor computes sha256(policy.json) locally, walks the anchor history until match.

plannedMarketplace-hosted public receipt validator

Phase D3: a `verify at railcall.ai` surface any third party can point at a receipt file + install pubkey to run the same 4-check pass the station's /api/receipts/verify runs, without needing shell access.

8. Auditor cheatsheet — what each receipt field proves

Every field is either a cryptographic derivative of content, or metadata that does not disclose content. This table is what a healthcare lawyer needs to satisfy themselves that the receipt does not enter §164.514 scope.

Receipt fieldWhat it proves
signature (Ed25519, key_id)Non-repudiation: these exact bytes came from this install
workflow_id + step_idAttribution: which authored workflow triggered the call
module_id + module_versionProvenance: which signed publisher's code made the call
caller_kindChannel: workflow / mcp / vscode / studio / cli
destination.provider + .model + .endpoint_url_hostWhere the data went (metadata only — host, not URL)
destination.trust_tierWhether the target was local / BAA / frontier / unknown
destination.requested_provider + .requested_modelWhen present: what the module asked for BEFORE a policy reroute
payload_hmac_hexFingerprint of the forwarded prompt without disclosing content
payload_hmac_original_hexWhen present (decision=redacted): fingerprint of the ORIGINAL prompt — comparing to payload_hmac_hex proves redaction happened
byte_count + field_countShape of the prompt (structural metadata only)
token_count_estimate + token_count_actualVolume of the call, both pre-flight estimate and post-response actual
policy_id + policy_version_hashGovernance: which policy doc was in force at emit time
decisionEnforcement outcome: allow / redacted / approved / denied / rerouted / would_*
approver_ref + approver_channelDual-control: who approved (if the approval path fired)
classificationWhat the classifier detected in the prompt (aggregate counts, never content)
timestamp_utcWhen the call happened (self-asserted)
timestamp_rfc3161TSA-anchored proof of when — verifiable off-box against freetsa.org
sequence_number + prev_hashPosition in the audit chain (rebuild-able index)

9. Verify a receipt end-to-end

External verification, from a different machine than the operator’s (an auditor, an internal SOC, or a customer’s security team):

# fetch the install pubkey (published by the station)
curl http://<operator-station>:8799/api/signing/pubkey

# fetch the receipt
curl "http://<operator-station>:8799/api/receipts/read?id=egress/eg_..."

# or use the shortcut endpoint — returns PASS/FAIL from 4 native checks
curl -X POST "http://<operator-station>:8799/api/receipts/verify" \
  -H "Content-Type: application/json" \
  -d '{"id": "egress/eg_..."}'

Both paths land the same verdict. The endpoint is a convenience; the receipt is verifiable off-box by anyone with the pubkey using standard Ed25519 tooling — no RailCall dependencies required.

Full canonical source (with changelog + open decisions): docs/phase_c_threat_model.md. Reviewers running procurement questionnaires can request the compliance bundle (SRA, adopted policies, subprocessors) under NDA from /trust.