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.
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-keygenstep, 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:
| Platform | Minimum | Notes |
|---|---|---|
| Intel TDX | Sapphire Rapids (4th-gen Xeon) or newer | Most cloud confidential offerings (Azure NCC, GCP CC) target TDX. |
| AMD SEV-SNP | EPYC 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
nvtrustSDK 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 reportON).
Inference engine inside the CVM
You run a local inference engine in the same CVM as the node — either:
llama-serverfrom llama.cpp, supervised by the node's local provider (recommended; the same engine documented in Serving models & pricing and used by thelocalprovider). Setllm.provider: local.- vLLM with
--enforce-eagerfor 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 thellama-serverpath.
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. Iftee.modeis notnone, the node refuses to start withopenai_passthroughorvertexai(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 != nonerequiresllm.provider: local(the rule above).tee.modemust be one ofnone,stub,nvidia-cc-tdx,nvidia-cc-snp. Unknown values are rejected.nras_urlis required for thenvidia-cc-*modes (stub skips this).refresh_intervalmust be positive.1his 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):
| Variable | Maps to |
|---|---|
NODE_TEE_MODE | tee.mode |
NODE_TEE_ATTESTATION_NRAS_URL | tee.attestation.nras_url |
NODE_TEE_ATTESTATION_REFRESH_INTERVAL | tee.attestation.refresh_interval (Go duration: 1h, 30m, 15m) |
NODE_TEE_ATTESTATION_EVIDENCE_CACHE_PATH | tee.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:
-
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_idinto the CVM config (zs.operator_idin YAML, orNODE_ZS_OPERATOR_IDin env) — the attestation binds to this id, so the CVM must know it at boot. -
Boot the CVM with
tee.mode != none. The node, running inside the enclave, generates a freshage(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. -
The node mints attestation evidence whose
report_datafield isSHA-256(node_pubkey || operator_id)— wherenode_pubkeyis your on-chain signing key — served fromGET /v1/zs/attestation. The binding is to your stable identity key, not to the rotating ephemeral recipient, soreport_datadoes not change when the sealing key rotates. The node still re-mints evidence on therefresh_intervalcadence to keep the bundle fresh, but the bound value stays constant across ephemeral rotations. -
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'sreport_data. The hardware refuses to sign areport_datathe 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}'
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
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_pathon 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
nvtrustversions 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.
- Capability advertisement.
GET /v1/zs/details→ theteefield should report your configuredmodeand anattested_atwithin the last2 × refresh_interval. Anything older means the refresh loop is wedged. - Evidence is fetchable.
GET /v1/zs/attestation→200with a well-formed bundle.503means stale or cold-start;404means the node isn't in TEE mode. - The advertised ephemeral is signed.
/v1/zs/detailscarriesephemeral_age_pubkeyand anephemeral_sigthat verifies under your on-chain signing address. (Nothing is published on chain to match — the signature is the anchor.) report_databinding is consistent — it equalsSHA-256(node_pubkey || operator_id)for your on-chain signing address, the same check the proxy runs.- A proxy accepts you — point a proxy at the same registry and confirm it logs no verification failure for your operator.
- An end-to-end Require-TEE request lands — a request carrying
X-Zs-Require-TEEis 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:
| Surface | What to watch |
|---|---|
GET /v1/zs/attestation | Should 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.