Threat model¶
This document enumerates the threats the SKI Framework reference implementation defends against, the assumptions behind those defences, and the threats explicitly out of scope. It is intentionally specific so that auditors can verify each control end-to-end.
The framework is presumed to be operated by a regulated organisation inside its own infrastructure boundary. The threat model is written from the operator's perspective.
Trust model¶
| Entity | Trusted for | Not trusted for |
|---|---|---|
| Operator (the regulated org) | Running the stack, holding KG signing keys, reviewing FLAG/DISCRETIONARY verdicts | Modifying the audit ledger, bypassing append-only triggers, downgrading conformance |
| KG editor | Producing the signed Knowledge Graph | Producing telemetry, deciding rule routing at runtime |
| Telemetry source (SCADA, sensors, ETL) | Producing well-formed telemetry records | Deciding which rule applies (the Tag Registry decides) |
| External auditor | Reading the audit ledger to re-verify | Modifying the ledger or its hash chain |
| SKI Framework maintainers | Publishing signed releases | Accessing operator data; the framework never phones home |
| LLM weights publisher (Ollama, etc.) | Providing the model artifact | Modifying behaviour after deployment; the SHA-256 pin (SKI_MODEL_FILE_SHA256) prevents silent substitution |
Trust boundary & OT deployment assumptions¶
This section is load-bearing for OT/ICS deployments — read it before you deploy. It makes explicit a narrower, easy-to-miss trust boundary implied by, but not spelled out in, the "Telemetry source" row above.
SKI's guarantees are stated relative to the telemetry it receives. The framework enforces its Ed25519-signed Knowledge Graph, its independent Symbolic Verifier, and its append-only audit ledger — but it does not, by itself, authenticate the telemetry producer or cryptographically verify a measurement's timestamp. Per the trust model above, the telemetry source (SCADA, sensors, ETL) is trusted to produce well-formed records; it is explicitly not trusted to decide which rule applies. What follows makes the remaining trust explicit.
Assumption 1 — telemetry integrity and authenticity are enforced
upstream of the sidecar. SKI assumes the transport delivering telemetry
to the sidecar (mTLS, a private/segmented OT network, a signed producer,
a historian with its own access control) already establishes that a
record genuinely came from the sensor/PLC/historian it claims to. The
sidecar (reference-implementation/src/sidecar) does passive, read-only
intake and forwards normalised records to the SKI Model service over
mTLS; it does not itself authenticate the original producer, and nothing
downstream re-derives that authentication.
Assumption 2 — the telemetry timestamp is authoritative, within a
bounded, enforced skew. Per the architecture's "Authoritative clock"
invariant (see Architecture), a telemetry record's own
timestamp field — never wall-clock-at-arrival — is the "now" used for
stateful predicates (window queries, freshness gates) and effective-date /
jurisdiction scoping. The runtime now cross-checks that field against
arrival wall-clock for exactly one purpose: bounding acceptable drift. The
per-tenant max_clock_skew_seconds column in the telemetry-buffer schema
(default 60s; see RFC 0001) is read at
startup — SKI_MAX_CLOCK_SKEW_SECONDS overrides it when no tenant row is
configured — and /api/evaluate checks every record against it before the
buffer write or KG scoping. A record outside the bound is never treated as
fresh: by default it is routed to DISCRETIONARY, ledgered (never
silently dropped), and excluded from the buffer; SKI_CLOCK_SKEW_MODE=reject
rejects it outright with 422 for operators who prefer that posture. Every
violation increments ski_telemetry_clock_skew_total. Setting
max_clock_skew_seconds=0 disables the guard (a documented opt-out, not a
silent one).
Residual risk this narrows but does not close. The guard bounds
drift — it cannot detect a well-resourced producer that forges a
timestamp close to real time. A producer that can forge its own clock
convincingly can still make fabricated data appear current, defeating
NULL_STALE routing and freshness-gated predicates (has_fresh_sample,
requires_recent_within_seconds) — see
conformance/provenance/test_null_stale_routing.py
for the mechanism this affects. This is a real gap SKI does not close on
its own: the framework's fail-closed guarantees are about the KG, the
verifier, and the ledger, not about the sensor's honesty about when a
reading was taken. The skew guard closes the gross case (a stale or
badly-drifted record cannot silently masquerade as current); it is not a
substitute for authenticated telemetry.
Recommendation for OT deployments. For essentially any live OT/ICS deployment, where this residual risk matters:
- Use signed or otherwise authenticated telemetry at the source
(device-signed payloads, a historian that itself enforces provenance,
or an ingestion gateway that attaches a verified capture timestamp)
rather than trusting the record's own
timestampfield at face value — the skew guard bounds drift, it does not authenticate the clock. - Use a trusted time source (NTP/PTP with monitoring, or a hardware time source) on the systems that stamp telemetry, so the authoritative-clock assumption above is actually sound upstream.
- Set
max_clock_skew_seconds(per tenant) orSKI_MAX_CLOCK_SKEW_SECONDSto the tightest bound your ingestion topology tolerates — legitimate historical replay/backfill needs a wider bound than live streaming telemetry does. - Tier obligations conservatively (
tier-1) wherever a spoofed reading could mask a real breach. Per spec §5.4, an undeclared or unrecognised risk tier already fails safe totier-1(seepolicies/risk_tier.py), which forces any non-AGREEDverifier result toDISCRETIONARYwith human attestation required — the strongest posture SKI's policy layer can offer against a telemetry-side compromise it cannot itself detect.
See also Limitations & assumptions for the companion point about non-formalizable rules, and docs/security.md for how this fits the project's broader security posture.
In-scope threats¶
T-1: Tampering with recorded verdicts¶
Goal: an insider rewrites a FLAG verdict to CLEAR to hide a breach.
Controls:
- Postgres BEFORE UPDATE / DELETE / TRUNCATE triggers on
ledger_entriesandtelemetry_bufferraise an exception. (reference-implementation/src/ledger/append_only.sql) - The audit ledger entry hash chains to the prior entry; any post-hoc edit breaks the chain.
- The canonical serialization is documented (
audit_ledger/canonical.py) so an external auditor can re-verify independently. - Database role separation: the
ski_modelservice connects with a role that hasINSERTonly on the ledger tables;UPDATE/DELETEwould require a different role with elevated privileges.
Residual risk: a sufficiently privileged DBA can disable the triggers. Mitigated by Postgres audit logging (operator responsibility) and out-of-band ledger archival (Level 3 conformance).
T-2: Loading a tampered Knowledge Graph¶
Goal: an attacker swaps the KG file to weaken or invert the rules.
Controls:
- Ed25519 signature verification on KG load. The SKI Model service
refuses to start with
KG signature verification FAILED. KG_REQUIRE_SIGNATURE=falseis permitted only for local demos and immediately disqualifies the deployment from any conformance level (specification B2.4).- Public key is mounted read-only into the container.
Residual risk: the signing key is held offline by the KG editor; key custody is the operator's responsibility. Loss of the private key permits forged KGs.
T-3: Non-deterministic inference output¶
Goal: the local LLM drifts (model substitution, library version change, GPU non-determinism) so the same input produces different verdicts.
Controls:
SKI_MODEL_FILE_SHA256is checked against the actual model file at service start. Mismatch -> refuse to start (specification B3.4).- Inference uses
temperature=0, fixedseed=42, structured output enforcement. - The Symbolic Verifier independently re-checks every
formalizable assertion the LLM emits; a drifted or substituted model
surfaces as
LLM_CONTRADICTION/NEURO_SYMBOLIC_DIVERGENCEstatuses in the verdict envelope. - The agreement monitor tracks the rolling LLM-verifier agreement rate and alerts on a sustained drop below threshold (default 0.95).
- Every envelope records the model-weight hash, KG version hash,
prompt-template hash, and decoder seed;
audit-ledger replayre-evaluates the symbolically verifiable part and verifies the signed transcript for the rest.
Residual risk: assertions outside the formalizable subset are
UNVERIFIABLE by construction; defensibility for them rests on the
signed transcript (reconstructible provenance), not re-generation.
T-4: Producer claiming a rule_id¶
Goal: a compromised producer attaches rule_id: "lenient_rule" to
its telemetry, hoping the runtime will trust it.
Controls:
- The sidecar and
send-telemetry.pyreject any record containing arule_idkey (client-side,B4.3). - The SKI Model server ignores any
rule_idit receives and always asks the Tag Registry to resolvesubject -> rule. - Conformance test
test_demo_telemetry_has_no_rule_idenforces this on the demo telemetry.
Residual risk: none for the documented APIs; an operator who bypasses the sidecar and writes directly to the SKI Model can still have their rule_id ignored, so this is fully mitigated.
T-5: Data exfiltration via cloud LLM¶
Goal: operational data leaks because the runtime calls a cloud API.
Controls:
- Default backend is
ollama(local). Theanthropicbackend is labelled non-conformant in.env.exampleand the SKI Model logs a bright warning if it is selected. - The
ski-internalDocker network has no external connectivity in the air-gapped profile (networks.ski-internal.internal: true). - The service makes no outbound network calls during inference when
the default backend is used. Verified by the
sovereignty/test_no_outbound_callsconformance test and the runtimetest_no_egresssuite.
Residual risk: the operator can intentionally choose a cloud backend. This is a policy violation, not a framework bug.
T-6: Replay-attack against the ledger¶
Goal: an attacker replays a CLEAR verdict to mask a later FLAG.
Controls:
- Each ledger entry includes
sequence_number(monotonic, unique) andtelemetry_id(unique per record). A duplicate insertion would collide on the unique constraint. - The chain hash binds each entry to its predecessor; a replayed entry would break the chain.
Residual risk: none.
T-7: Secrets leakage¶
Goal: an attacker reads the Postgres / API / Grafana password.
Controls:
scripts/setup.shgenerates strong random secrets and writes.envwith mode0600.- No defaults are present; the stack refuses to start without secrets
(Docker compose uses
${VAR:?error}syntax). .envis in.gitignore.- TLS certificates (self-signed by setup) live under
tls/with modego-rwx.
Residual risk: operator-side handling of .env is the operator's
responsibility.
T-8: Supply-chain compromise¶
Goal: a malicious dependency is pulled in via pip.
Controls:
- All dependencies pinned in
requirements-dev.txtand per-toolrequirements.txt. - Dependabot is enabled with grouped minor/patch updates and a monthly cadence.
- CI runs
pip-auditand Trivy against every PR. - CycloneDX SBOM generated for each release.
- (v0.3 planned) Sigstore / cosign signing of release artifacts and container images; SLSA Level 3 provenance.
Residual risk: a typosquat slipping past pip-audit. Mitigated by
review-before-merge.
Out of scope¶
The following are not defended against by the framework itself. They are the operator's responsibility.
- Host OS compromise. If the host running Docker is rooted, all bets are off.
- Insider with full Postgres superuser. A DBA can disable triggers; mitigation is administrative, not technical.
- Compromise of the offline signing key. Key custody is the operator's responsibility.
- Side-channel attacks on the LLM. Timing / cache attacks on the inference path are not in scope. The Symbolic Verifier's checks are pure predicate evaluation with no LLM in the path and are unaffected.
- Denial of service. Rate limiting and admission control are out of scope for the reference implementation; production deployments are expected to put the SKI Model behind an ingress controller that handles this.
- Compromise of upstream LLM weights publishers. The SHA-256 pin prevents silent substitution but cannot detect a backdoor in the originally-published weights.
Re-verification¶
An external auditor can independently verify every control above:
- Append-only triggers: query
pg_triggerand read the trigger bodies. RunUPDATE ledger_entries SET verdict = 'CLEAR' WHERE sequence_number = 1— it must error. - Signature requirement: corrupt one byte of
kg.json; the SKI Model service must refuse to start. - Agreement monitor:
GET /api/canary; confirmstatusishealthyandagreement_rateis at or above the threshold. - Replay:
audit-ledger replay --from 1 --to N --kg-path kg.json --strict. Must exit zero; symbolically verifiable parts replay exactly, LLM reasoning re-verifies via the signed transcript. - Chain integrity:
audit-ledger verify. Must reportchain_link_verified = N / N.
If any of these checks fails on a deployment claiming conformance, the operator's claim is invalid.