Skip to main content

Confidential compute (TEE)

Standard mode protects the wire: prompts are end-to-end encrypted and only your node can open them (see Encryption & keys). But once your node decrypts a prompt to run the model, you — the operator — can see the plaintext. The design openly trusts you with that. Confidential mode removes that trust: your node runs inside a hardware-attested confidential VM (CVM), the decryption key is generated inside the attested hardware, and you get a cryptographic proof — not a policy promise — that the operator cannot read what it serves.

This page is the operator's hands-on companion for running a node in confidential mode: what it buys, the hardware it needs, the tee: config block, and the in-CVM key bootstrap that ties it all together.

warning

Implementation status — read this first. The confidential-compute surface is designed and partially shipped. The configuration (tee: block), the /v1/zs/attestation endpoint, the tee advertisement on /v1/zs/details, and the proxy-side verifier are all in place today. But the only fully wired tee.mode in the current build is stub — a deterministic, well-formed-but-untrusted evidence bundle for laptop development.

Selecting nvidia-cc-tdx or nvidia-cc-snp today fails the startup check with a clear "not yet implemented" error. Real Intel TDX / AMD SEV-SNP quote fetching and NVIDIA EAT minting land in later releases behind the same config — your tee: block won't change when they ship. Treat the hardware modes as forthcoming, not production-ready. Everything below describes the finished feature so you can plan for it and validate the full pipeline with stub mode now.

What confidential mode buys you

The relying party — the proxy that routes a sensitive request — gets cryptographic proof that the prompt is sealed to a public key whose private half lives inside attested hardware you cannot read.

The operator-relevant guarantees:

  • The decryption key is born inside the CVM. The node generates a fresh age (X25519) ephemeral identity using the in-enclave RNG — on boot and on every ~20-minute rotation. The private half never exists outside CVM-encrypted memory — there is no host-side age-keygen step, and nothing for you to copy in.
  • The proxy verifies your hardware before it seals anything. It checks your TDX or SEV-SNP report and your GPU's NVIDIA EAT against the published reference build before it will encrypt a single TEE-required prompt to your advertised key.
  • Your binary's measurement is part of the attestation. Modifying the node code — for example, adding prompt logging — changes the measurement, and your attestation no longer matches the reference build. The proxy stops sealing to you. You cannot quietly patch in a way to read prompts.
  • Local inference is mandatory. The node refuses to start a TEE node that forwards prompts off-host (see the hard rule).

Compared to standard mode, this is an upgrade of the trust model described in Encryption & keys: "you can see the decrypted prompt" becomes "you cannot."

What it does not hide

Confidential mode is not a cloak of invisibility. It seals content, not everything around it:

  • Metadata is still visible — ticket IDs, model names, token counts, and latency. Billing requires them, so you and the proxy both see them.
  • Side channels are out of scope — timing, power, and microarchitectural leakage are not defended by attestation. Keep your CPU microcode patched.
  • Liveness is still yours to control. You can refuse to serve a request — you just can't read what you do serve.

Hardware and platform requirements

Confidential mode needs both a CPU TEE and an NVIDIA GPU with Confidential Computing (CC). One without the other does not give the full guarantee — the CPU TEE protects host memory, but inference happens in GPU HBM, which only GPU CC protects.

CPU TEE

Either of:

PlatformMinimumNotes
Intel TDXSapphire Rapids (4th-gen Xeon) or newerMost cloud confidential offerings (Azure NCC, GCP CC) target TDX.
AMD SEV-SNPEPYC Genoa (Zen 4) or Milan (Zen 3)More common for self-hosted bare metal.

Earlier generations do not qualify. Intel SGX (enclave-only, not whole-VM encryption) and AMD SEV-ES (no attested measurement of the full image) are not supported — the threat model requires whole-VM memory encryption and an attested measurement of the entire OS image.

GPU TEE

NVIDIA Confidential Computing requires H100, H200, or Blackwell (B100/B200). A100 is not supported — it has no on-die security processor.

You also need:

  • NVIDIA driver ≥ 535 with CC support compiled in.
  • The nvtrust SDK accessible to the node (used to generate the GPU EAT). On Phala/dstack it's bundled; on Azure NCC it ships in the confidential-VM image.
  • A GPU bootstrapped in CC mode at provisioning time. CC mode is a host-level configuration set before the CVM starts — it is not toggleable per request. Confirm it from inside the CVM with nvidia-smi conf-compute -f (must report ON).

Inference engine inside the CVM

You run a local inference engine in the same CVM as the node — either:

  • llama-server from llama.cpp, supervised by the node's local provider (recommended; the same engine documented in Serving models & pricing and used by the local provider). Set llm.provider: local.
  • vLLM with --enforce-eager for production-scale serving on H100+. Eager mode is required because vLLM's CUDA-graph compilation does not yet work cleanly with GPU CC (~3–5% throughput hit). A dedicated provider for an in-CVM vLLM sidecar is on the roadmap; until it ships, use the llama-server path.

The node and its inference subprocess must run inside the same CVM, talking over loopback or a Unix socket. Splitting them across VMs — even confidential ones — would put the plaintext prompt on a network the host can see.

The mandatory local-inference rule

This rule is non-negotiable and enforced at startup:

A TEE node must use llm.provider: local. If tee.mode is not none, the node refuses to start with openai_passthrough or vertexai (or any other remote/forwarding provider).

Forwarding plaintext to a third-party API would leak the very prompts the TEE exists to seal, so the config validator hard-rejects it:

tee.mode="nvidia-cc-tdx" requires llm.provider=local — forwarding plaintext
to "openai_passthrough" would leak the very prompts the TEE is meant to seal;
use the supervised local provider instead

If you need a remote LLM, run a separate, non-TEE node alongside the confidential one.

The tee: config block

The whole opt-in lives under one new top-level YAML block, alongside the existing zs: and llm: sections (see Configuration):

tee:
# mode: which confidential-computing platform this node runs on.
# none - default; standard trust model, not a TEE node.
# stub - dev only; deterministic, well-formed-but-untrusted
# evidence. Exercises the whole pipeline on a laptop with
# no special hardware. Proxies reject stub evidence in
# production unless explicitly opted in.
# nvidia-cc-tdx - Intel TDX host + NVIDIA GPU CC. (forthcoming)
# nvidia-cc-snp - AMD SEV-SNP host + NVIDIA GPU CC. (forthcoming)
mode: nvidia-cc-tdx

attestation:
# NVIDIA Remote Attestation Service endpoint. Leave at the default unless
# you run a private attestation deployment.
nras_url: https://nras.attestation.nvidia.com

# How often the node mints fresh attestation evidence. /v1/zs/attestation
# serves the cached bundle and re-mints on this cadence. Evidence older than
# 2x this interval is served as 503 (stale), and the proxy demotes you out of
# the TEE-eligible set until fresh evidence is available.
refresh_interval: 1h

# Where to persist the latest evidence bundle so a brief restart doesn't drop
# you out of the verified set. MUST live on a CVM-encrypted volume — the
# bundle binds to your current ephemeral recipient and the host operator must
# not be able to read past evidence. Unset = mint fresh on every boot.
evidence_cache_path: /var/lib/zs-node/attestation/last.json

Validation at startup

The config validator enforces, and refuses to start on failure:

  • tee.mode != none requires llm.provider: local (the rule above).
  • tee.mode must be one of none, stub, nvidia-cc-tdx, nvidia-cc-snp. Unknown values are rejected.
  • nras_url is required for the nvidia-cc-* modes (stub skips this).
  • refresh_interval must be positive. 1h is the recommended start; tighter intervals (15m, 30m) are valid but increase NRAS load.

Environment variables

Every field has a NODE_TEE_* override (env wins over YAML):

VariableMaps to
NODE_TEE_MODEtee.mode
NODE_TEE_ATTESTATION_NRAS_URLtee.attestation.nras_url
NODE_TEE_ATTESTATION_REFRESH_INTERVALtee.attestation.refresh_interval (Go duration: 1h, 30m, 15m)
NODE_TEE_ATTESTATION_EVIDENCE_CACHE_PATHtee.attestation.evidence_cache_path

There are no env-only secrets in the tee: block — the whole thing is safe to commit to YAML. The binding to your specific node comes from the in-CVM generated key and the attestation report itself, neither of which is configured here.

How the in-CVM key binds to your attestation

This is the load-bearing operational detail. Get it wrong and your attestation is meaningless. The good news: there is nothing extra for you to do by hand. Confidential mode reuses the exact same key handling as a standard node — it just adds a hardware proof on top.

In the standard install the node mints and rotates an ephemeral sealing recipient entirely in memory, signs it under your on-chain signing key, and advertises it on /v1/zs/details; you never touch it (see Encryption & keys). Confidential mode keeps all of that and adds one guarantee: the ephemeral key is generated inside the CVM, never on the host — a key the host generated is one the host could have logged, which would defeat the point of attestation.

Crucially, the encryption key is never published on chain. Your on-chain record anchors only your identity — the Ed25519 signing key — and the ephemeral recipient is bound to that identity by a signature, not by a registry write. So there is no placeholder pubkey to register, no owner-signed pubkey rotation, and no updateOperator call in this flow. Here's what actually happens:

  1. Register normally. Operator and node registration take your owner/signing addresses, base URL, and stake — no encryption key (the register-node form has no pubkey field; see Registering on-chain). Bake the issued operator_id into the CVM config (zs.operator_id in YAML, or NODE_ZS_OPERATOR_ID in env) — the attestation binds to this id, so the CVM must know it at boot.

  2. Boot the CVM with tee.mode != none. The node, running inside the enclave, generates a fresh age (X25519) ephemeral recipient from the in-enclave RNG, signs it under your on-chain signing key, and advertises it on /v1/zs/details (ephemeral_age_pubkey + ephemeral_sig) — exactly as a standard node does. The private half lives only in CVM-encrypted RAM.

  3. The node mints attestation evidence whose report_data field is SHA-256(node_pubkey || operator_id) — where node_pubkey is your on-chain signing key — served from GET /v1/zs/attestation. The binding is to your stable identity key, not to the rotating ephemeral recipient, so report_data does not change when the sealing key rotates. The node still re-mints evidence on the refresh_interval cadence to keep the bundle fresh, but the bound value stays constant across ephemeral rotations.

  4. The proxy verifies the binding. Before sealing TEE-required traffic, the proxy computes SHA-256(node_pubkey || operator_id) from your on-chain signing address and the operator id, and confirms it equals the attestation's report_data. The hardware refuses to sign a report_data the in-enclave binary didn't request, so the binding can't be forged from outside the CVM. The anchor is your on-chain signing identity: an operator can't attest a real CVM under one identity while serving traffic as another, because the same signing key both authenticates the attestation binding and signs the advertised ephemeral recipient (ephemeral_sig).

The same four steps, drawn out:

You can read the live values from outside to sanity-check a deployment, but none of this requires an action from you:

NODE_URL=https://your-node.example.com
curl -s "$NODE_URL/v1/zs/details" | jq '{ephemeral_age_pubkey, ephemeral_sig, tee}'
curl -s "$NODE_URL/v1/zs/attestation" | jq '{report_data, generated_at}'
info

No persistence required. The ephemeral private key lives only in CVM-encrypted RAM, is never written to disk (not even to a CVM-encrypted volume), and is zeroed on each ~20-minute rotation. A fresh key on every boot is the normal, by-design case — the CVM's RAM encryption protects it for its lifetime, and the node simply re-attests the new ephemeral. The optional evidence_cache_path persists only the attestation evidence bundle (so a brief restart doesn't drop you from the verified set while fresh evidence is minted) — never the key itself.

Key rotation

There is nothing to schedule. The node rotates its in-CVM ephemeral on the same fixed cadence as a standard node. The report_data binding is to your stable signing key, so it doesn't change when the ephemeral rotates — the node simply keeps re-minting fresh evidence on its refresh_interval. No maintenance window, no owner-signed step, no on-chain update.

Validate the whole pipeline with stub mode

Before you pay for H100 / CVM time, exercise the entire confidential-mode path on your laptop with tee.mode: stub. Stub mode produces a deterministic, well-formed-but-untrusted bundle so every component on both sides — the node's config gate, the /v1/zs/details advertisement, the /v1/zs/attestation handler, the proxy verifier, the routing filter, and the dashboard badge — runs the same code it will run in production. The only thing missing is the real hardware-rooted evidence chain.

Add the block to the same config.yaml a non-TEE deployment already uses:

tee:
mode: stub
attestation:
refresh_interval: 1m
evidence_cache_path: /tmp/zerosignal-tee-stub.json

The validator still requires llm.provider: local, so point llm.local at your llama-server binary with one model configured (see Serving models & pricing). Start the node and confirm what it advertises on the public port (9090 by default):

NODE_URL=http://localhost:9090

curl -s "$NODE_URL/v1/zs/details" | jq .tee
# { "mode": "stub", "attested_at": "...", "evidence_url": "/v1/zs/attestation" }

curl -s "$NODE_URL/v1/zs/attestation" | jq .
# { "mode": "stub", "node_pubkey": "...", "operator_id": 42, "report_data": "...", "stub": true, ... }

Both should return 200. A 404 on /v1/zs/attestation means the node didn't pick up tee.mode: stub — check the startup logs for a validator error.

Stub evidence is rejected by production proxies. A real proxy marks a stub-mode operator unverified and only drops it when a request actually requires TEE (a sensitive route carrying the X-Zs-Require-TEE header). Plain non-TEE traffic to the same operator still routes normally — the gate is per-request, not a blanket block. To exercise the gate end-to-end against your own dev proxy you either send that header or opt the dev proxy into accepting stub bundles; that opt-in is dev-only and must never be set in production.

What stub mode does not validate: the real TDX/SEV-SNP signature chains, the NVIDIA EAT / NRAS round-trip, the CVM memory-encryption guarantee, or the encrypted-volume requirement for the key (stub keeps it in plaintext on your filesystem). It validates the plumbing, not the hardware.

Deployment recipes

warning

Coming soon. Copy-pasteable recipes — pinned compose files, the GPU-bundled image build, per-platform walkthroughs — ship together with the TDX/SEV-SNP hardware modes (see the status note at the top). Until then, stub is the only mode that runs end-to-end. What follows is the roadmap of paths the recipes will cover, so you can plan hardware now.

Once stub validation passes, the real deployment swaps tee.mode: stub for the platform string and removes any dev-only stub opt-in from the proxy. The paths the recipes will cover:

  • Phala Cloud / dstack — CVMs on TDX hosts with NVIDIA H200, where the compose hash becomes the attested measurement. The lowest-friction path for a fresh deployment; closely modeled on Phala's open private-ml-sdk.
  • Azure NCC H100 v5 — TDX hosts with H100 GPUs in CC mode, via a confidential VM (--security-type ConfidentialVM). Best when you want managed Kubernetes or region-specific data residency.
  • Bare-metal SEV-SNP — EPYC Genoa/Milan with SEV-SNP in BIOS (use tee.mode: nvidia-cc-snp) and a CVM-aware hypervisor.
  • GCP H100 confidential — TDX-based; identical config to the Azure recipe.

Pitfalls that apply to every path, worth knowing up front:

  • Pin every image by digest, never by tag. The compose / VM image hash is what the CPU report measures. A floating tag produces a different measurement on every pull, and verifiers refuse to seal to a moving target.
  • Keep the key and evidence_cache_path on a CVM-encrypted volume. A plain host bind-mount is readable by the host operator — which would leak the very key the attestation promises is sealed.
  • Allow outbound 443 to NRAS (nras.attestation.nvidia.com) from the CVM, or GPU-EAT verification fails.
  • Pin the driver and nvtrust versions together inside the CVM image — the combination is part of the attested measurement.

The tee: config and the verification steps below won't change when the hardware modes land — only the mode string you set.

Verifying a deployment from outside the CVM

Run this from a workstation that is not the CVM host — the point is to confirm what a third party sees.

  1. Capability advertisement. GET /v1/zs/details → the tee field should report your configured mode and an attested_at within the last 2 × refresh_interval. Anything older means the refresh loop is wedged.
  2. Evidence is fetchable. GET /v1/zs/attestation200 with a well-formed bundle. 503 means stale or cold-start; 404 means the node isn't in TEE mode.
  3. The advertised ephemeral is signed. /v1/zs/details carries ephemeral_age_pubkey and an ephemeral_sig that verifies under your on-chain signing address. (Nothing is published on chain to match — the signature is the anchor.)
  4. report_data binding is consistent — it equals SHA-256(node_pubkey || operator_id) for your on-chain signing address, the same check the proxy runs.
  5. A proxy accepts you — point a proxy at the same registry and confirm it logs no verification failure for your operator.
  6. An end-to-end Require-TEE request lands — a request carrying X-Zs-Require-TEE is routed to your node and returns a normal completion.

Monitoring

The standard monitoring on the private listener (/healthz, /livez, /metrics on 9091) still applies. Confidential mode adds two things to watch:

SurfaceWhat to watch
GET /v1/zs/attestationShould respond 200 with generated_at newer than 2 × refresh_interval. A 503 (stale) means the refresh loop is failing — check node logs for the underlying NRAS / nvtrust error.
GET /v1/zs/details (tee field)Should report your configured mode and an attested_at matching the most recent successful refresh.

Make sure /v1/zs/attestation is reachable on your public HTTPS URL (if a TLS reverse proxy fronts the node, expose it there) — proxies fetch it during their per-operator details poll, and the verifier needs it to keep you in the TEE-eligible set.

Where this fits

  • Confidential mode is an upgrade of the trust model in Encryption & keys — from "the operator can read the decrypted prompt" to "the operator cannot."
  • The mandatory-local-inference rule ties into provider selection in Serving models & pricing; the tee: block sits alongside the rest of your Configuration.
  • Confidential nodes still relay and are still relayed for, unchanged (see Relays) — relaying never touches plaintext on either side.