Skip to content

Threat model disclosure

The document a security review asks for. It restates the threat model in the shape a vendor questionnaire expects: trust boundaries, a STRIDE map, the risks left with you, and the artifacts a scanner can read. The engineering page is the source. Four regions of this one are lifted or checked against it by make gen-docs, so the two cannot disagree.

pistra is a self-hosted inline enforcement point. It sits in the request path between your applications and the model providers they call, which puts it in the blast radius of both. Review it as a Tier 1 network proxy that also holds credentials.

Responsibility changes at each one.

Four bands stacked in the order a request crosses them: your network up to the listener, process memory and the node's disk, the egress connection pistra makes, and the admin plane on its own door; the memory band is marked

A, the internal network. Your applications and agents to the pistra listener. They authenticate with a virtual key pistra minted or a token your identity provider signed. pistra terminates the TLS on this hop when it is the front door, and does not when it runs behind your own gateway.

B, the enforcement engine. Process memory and the local raft store. Policy is evaluated here, detectors run here, and provider credentials are resident here. The raft log is on the node’s disk. It is not encrypted wholesale: named values are sealed individually, which the asset table below states per asset.

C, the egress. pistra to the model providers. pistra makes this connection and owns its TLS. The credential presented on it is the provider key held at boundary B, never the client’s.

D, the admin plane. Operators, your identity provider, and, on Kubernetes, the operator and controller. It is reached on a different listener, authenticated by a different credential, and decided by a different engine from anything at boundary A.

Nothing. There is no licence check, no activation, no usage report and no diagnostic upload. The licence carries no metering code, so there is no build in which one is compiled out.

The binary does make outbound connections you configure, which is why the paragraph above says no vendor endpoint rather than air-gapped. This is the whole list: the model providers you route to; your OTLP collector, when telemetry is exported; huggingface.co, when a models entry resolves an hf: reference, which pistra models pin turns into a one-time resolution; a presidio-analyzer you run, when the remote detector is enabled; a provider’s admin usage API, when metering reconciliation is on; your identity provider’s JWKS endpoint; and an ACME directory, when the front door issues its own certificates. Every one of them is off unless the configuration turns it on.

One destination is a library’s rather than ours. ONNX Runtime 1.29 runs a telemetry uploader on Linux. guardrails/onnxrt sets the environment variable that disables it before creating an environment, and binds DisableTelemetryEvents, so the default in a pistra deployment is off. It sets rather than forces, because the loader takes whatever libonnxruntime is on the system and an operator who wants the telemetry keeps it. The nativecheck image target runs with nothing set, which is what makes the default checkable.

What is worth stealing, where it sits, and what holds it.

asset where it lives protection
provider API keys config file or env, in memory never sent downstream; swapped in after all mutations; the client’s own credential is stripped
virtual keys the raft log stored as SHA-256 only; the token itself is never written anywhere
cluster state raft log, boltdb ACME private keys sealed with AES-GCM (HKDF-SHA256 from the cluster secret) before they are proposed
prompt content in flight only see below

Each category names where the answer is written rather than restating it, because the restatement is what goes stale.

Category How it is answered Where it is written
Spoofing Keys are 32 bytes of entropy stored as SHA-256 and found by hash. Tokens are verified against the issuer’s JWKS and bound to the audience of the plane they present at. Roles come from an issuer-assigned claim. impersonation
Tampering Configuration is file-owned and comes from your repository. A catalog overlay can describe an endpoint and cannot invent a capability, a dialect or an action. A configuration that fails to build is rejected whole and the previous snapshot keeps serving. trust boundaries in configuration
Repudiation Every audit record is a link in a per-node hash chain signed with ed25519. Raft witnesses the head, so a node cannot rewrite its own history alone, and pistra audit verify checks an export offline. audit-trail
Information disclosure No log statement, trace attribute or stored column carries a prompt, a completion or a matched span. Detection and rewriting happen in memory on the stream. Two exceptions are named rather than left out. prompt content is not retained
Denial of service Spend is reserved before a request is forwarded and settled against real usage after. Every CEL condition runs under a per-evaluation cost budget and stops when the client goes away. what it defends against
Elevation of privilege The admin and data planes use different engines over different inputs and neither reaches the other. Admin decisions default to deny over a vocabulary closed at load. A profile can only narrow what the deployment allows. privilege escalation

This list is the threat model’s own, copied rather than summarised.

  • A determined exfiltrator. Pattern detection finds structured PII in the shapes it knows. It does not find paraphrase, base64, an unusual encoding, or a description of a secret. Treat guardrails as compliance and accident prevention, not as an adversary-resistant filter.
  • Prompt injection and jailbreaks. Tier 0 does not cover them at all. Two detectors do, and both reduce rather than prevent. The classify detector, see stopping prompt injection with a classifier, runs a checkpoint trained on the question and has no instructions to exploit. It catches the phrasings it was trained on and false-positives on prose about security. The llm judge, see judging with a model, takes a policy in prose, and reads exactly the text an attacker wrote. A model that follows instructions can be told what to answer. Treat either’s verdicts as a filter on the common case, run it in shadow until the false-positive rate has been read, and do not build a control on the assumption that it cannot be talked round.
  • Anything the gateway never sees. Traffic on the same provider key that does not traverse pistra is ungoverned. Reconciliation exists to detect this, not to prevent it.
  • A compromised gateway host. Provider credentials are in memory. A host with an attacker on it is over.
  • Whoever can write the configuration. Policy, roles and rules are file-owned, so the caller who can change that file, or restart the process against a different one, holds the highest privilege in the deployment. A built-in super-admin was rejected for that reason rather than omitted: it would have been a second escape beside one that cannot be closed. Govern the file the way you govern the cluster, and read admin.authz records for who changed what.
  • Whoever holds the cluster secret. The raft transport and the forward listener the nodes use for writes, membership and restores run under mutual TLS, with a certificate authority every node derives from the cluster secret and a per-node certificate under it that is replaced daily. Nothing on the path between the nodes reads or injects there, and a peer without the secret is refused at the handshake. The other side of that: the secret is the whole identity. A holder of it mints a certificate for any node name, joins the cluster, and reads every replicated document that is not sealed.
  • Whoever holds the unsealing material. The cluster secret, and any kek_file wrapping a key, decrypt everything the barrier sealed. They are the deployment’s root secret and no policy in the gateway constrains them.
  • A stolen Kubernetes credential in the operator’s pod. The operator and the InferencePool controller are the only components holding one. The grant is get on a Credential’s Secret in the namespaces being watched and never list, no patch, no watch and no delete, and each writes a source of its own through the admin API. That bounds what the credential reaches rather than removing the exposure.
  • Model output correctness. Not a security control and not claimed as one.
  • Bodies it could not read. Over max_body_bytes, unparseable, or, on inspected traffic, a response whose content-type is neither JSON nor SSE. All counted in pistra_guardrail_uninspected_total{route,reason} rather than assumed clean. Alert on it. A body that was not examined is not a clean one.
  • Entities split across frames of an unknown streaming API. On a route the segmenter has no dialect for, the inspector reads each SSE frame on its own rather than assembling them into one logical message. A match spanning two frames there is missed. Assembling them would invent entities spanning two unrelated messages, which is the worse error.

Controls that are real only if you operate them.

  • The configuration file. Policy, rules, profiles and roles are file-owned. Whoever can change that file, or restart the process against a different one, holds the highest privilege in the deployment. Review it the way you review cluster manifests.
  • The unsealing material. The cluster secret, and any kek_file wrapping it, decrypt everything the barrier sealed. Rotation is add, roll, drop, and rotating the cluster secret is the procedure.
  • The identity provider. It is the authority for admin identity and for agent identity, and pistra issues no tokens of its own. Its claims-to-roles mapping is a privilege decision made in your IdP.
  • The failure direction, per control. A detector that errors is skipped or refuses the request, and a rule whose condition cannot be evaluated does the same. Both default closed where the failure is reachable from the client side. Neither default is right for every deployment.
  • The uninspected counter. pistra_guardrail_uninspected_total counts bodies that were not examined, by reason. A body that was not examined is not a clean one, and nothing alerts on this unless you do.
  • The audit heads. Verification is offline and needs the witnessed heads. Verifying the audit trail is the procedure, and it is worth running before you need it.
  • Re-cutting a release after a scan. The weekly scan files an issue against the newest published image. Clearing it means rebuilding the same source on a current base, which is a decision only you can make.

Five artifacts ship with each release, each answering part of the question. The scope columns say where each one stops.

Artifact Tool What it covers What it does not
SBOM (SPDX and CycloneDX) syft the released image, catalogued from the image, then the two C++ libraries added from the pins the build verified them against whether any package listed is exploitable here, which is what the rows below answer
VEX (OpenVEX) govulncheck every Go module in the repository, by symbol reachability, so a statement says whether the vulnerable function is called and not merely whether the package is present everything outside Go: the base image, and the three native libraries
Rust advisories cargo-audit the tokenizer shim’s 78 crates against RUSTSEC, weekly; the same graph is embedded in the shipped library, so a scanner reading the image sees it too nothing in that graph, though an unmaintained crate is reported as a warning and does not fail the run
SAST (SARIF) gosec the Go source that ships, uploaded to GitHub code scanning on every push to main dependencies, tests and the in-repo generators, the Rust shim, and anything reached through dlopen
Vulnerability scan grype the newest published image, weekly, against a database that moves after a release is cut; Vectorscan is in scope through its Debian package identity ONNX Runtime, for the reason below, and reachability: it reads packages and versions

The image is signed with cosign keyless over its digest, and the SBOM and VEX name that digest, so a consumer can tie all three to the same bytes.

The tokenizer shim is ours, built from guardrails/tokenizer/shim with cargo-auditable, which writes the crate graph into the shipped library. syft reads it out of the image and cargo audit reads the same graph against RUSTSEC weekly.

Vectorscan is Debian’s libvectorscan5, copied out of a build stage. The version dpkg reported travels into the image with it, and the SBOM carries a Debian purl, so grype resolves it against the Debian security tracker.

ONNX Runtime is a release tarball, checked against a SHA-256 the Dockerfile pins. Nothing scans it. NVD holds no CPE for ONNX Runtime and OSV no advisory, so a scanner reading the SBOM matches nothing whatever identifier the component carries, and the component says that in a property instead of implying coverage. The control is a version watch: the weekly workflow compares the pinned version against upstream’s newest release and opens an issue when it falls behind.

Reporting a finding, and what counts as one, is at the end of the threat model. The repository is pre-release and interfaces may change without notice, which includes this document’s own structure.