Verified chat over confidential vLLM
Deploy an offline vLLM backend and send chat requests through an attestation-verified, encrypted session with your c8s router.
Build a Node chat client that verifies your c8s router and sends each prompt through the resulting encrypted session. The router runs in a Trusted Execution Environment (TEE), then forwards requests over the private mesh to vLLM's ordinary HTTP listener. The client protects application traffic even when an intervening proxy terminates the outer HTTPS layer.
This tutorial uses your own cluster and route. The verification model explains the trust chain from the router to its admitted backend.
Prerequisites
Prepare these inputs before starting:
- A running cluster from Your first confidential cluster,
with the default
c8srelease,c8s-systemnamespace, and admitted platform components. Keep its default router TLS mode and certificate namec8s-router.c8s-router.svc. c8s,kubectl, and Node 20.19 or newer on your workstation, plus a builtc8s-verifypackage implementingc8s/attest-pq/v1. Follow its installation instructions before starting; this page does not assume a publicly available npm release or a usable WASM build.- The reachable external Certificate Distribution Service (CDS) endpoint, its approved
cds-pin.jsonbinding its image and exact measured launch configuration,operator.key,operator.pub, and the retainedmesh-ca.pem. Copy the first tutorial'scds-bootstrap/cds-pin.jsontocds-pin.jsonin this working directory. - The first tutorial's approved
nodes-policy.jsonandrouter-pin.json. Obtain these complete CLI image policies from your trusted image producer or deployment operator through the Azure policy preparation procedure.nodes-policy.jsonmust describe the independently intended current node set CDS admits; obtain its approved replacement from the operator if admission policy has changed.router-pin.jsoncontains only identities acceptable for nodes hosting this router. - For this SNP path, an operator-supplied current AMD KDS certificate revocation list (CRL)
in binary DER, with confirmation that it matches the router nodes' processor generation.
Save it as
snp-crl.derin Step 4. Obtain it through the operator's validated vendor-collateral procedure before starting; missing collateral leaves this prerequisite unmet. - A digest-pinned CPU vLLM image that your image producer has validated with the manifest
below. Its entrypoint serves HTTP on
0.0.0.0:8000, loads a bundled model from/models/tutorial-model, and exposes the OpenAI chat model IDtutorial-model. Include the tokenizer and chat template in the image; startup and inference must work offline. The image runs as UID/GID1000, writes scratch data under/cacheand/dev/shm, and fits your nodes' CPU/RAM budget, including those two memory volumes. The nodes must be able to pull it. Obtain the image's HTTP readiness path from its producer: an unauthenticatedGETon port8000must return200only when the bundled model is loaded and ready for inference, and a failure status while it is unavailable. Use a clean absolute path with unreserved URL characters in each segment and no query. - A network path from this workstation to the router on TCP
443, through its existing node host port or routed Service address. Configure workstation DNS or a hosts entry soc8s-router.c8s-router.svcresolves to that reachable address. Any load balancer on this path must pass TLS through; pod port-forwarding is not a substitute.
Use public demonstration weights and non-sensitive prompts for this exercise. Bundling weights in a registry image does not provide encrypted model storage. A validated serving image, resource sizing, registry access, and DNS/reachability are required deployment inputs; this tutorial does not supply or runtime-test them.
Step by step
1. Deploy a confidential vLLM backend
Set the image reference and external CDS URL, replacing all angle-bracket placeholders:
export VLLM_IMAGE='<REGISTRY>/<REPOSITORY>@sha256:<IMAGE_DIGEST>'
export VLLM_READY_PATH='<IMAGE_READINESS_PATH>'
export CDS_URL='https://<CDS_HOST>:8443'
export CHAT_HOST='c8s-router.c8s-router.svc'<IMAGE_DIGEST> is the validated image's 64-hex manifest digest. <IMAGE_READINESS_PATH> is
the producer-confirmed readiness path from the image contract above. <CDS_HOST> is the VM
endpoint from installation. CHAT_HOST matches the default router certificate's DNS name.
Save the deployment using that image:
cat > vllm.yaml <<EOF_MANIFEST
apiVersion: apps/v1
kind: Deployment
metadata: { name: vllm, namespace: workloads }
spec:
replicas: 1
selector: { matchLabels: { app: vllm } }
template:
metadata:
labels: { app: vllm }
annotations: { confidential.ai/cw: vllm }
spec:
enableServiceLinks: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
seccompProfile: { type: RuntimeDefault }
containers:
- name: vllm
image: ${VLLM_IMAGE}
env:
- { name: HOME, value: /cache }
- { name: TMPDIR, value: /cache }
ports: [{ containerPort: 8000 }]
readinessProbe:
httpGet: { path: "${VLLM_READY_PATH}", port: 8000 }
periodSeconds: 5
timeoutSeconds: 2
securityContext:
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
volumeMounts:
- { name: shm, mountPath: /dev/shm }
- { name: cache, mountPath: /cache }
volumes:
- name: shm
emptyDir: { medium: Memory, sizeLimit: 1Gi }
- name: cache
emptyDir: { medium: Memory, sizeLimit: 1Gi }
EOF_MANIFEST
kubectl create namespace workloads --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -f vllm.yamlconfidential.ai/cw: vllm names the workload's Service intent: the operator creates the
headless Service c8s-vllm.workloads.svc.cluster.local. Every covered pod receives mesh
injection. The webhook adds three init entries: persistent c8s-mesh and c8s-cert sidecars,
and the c8s-cert-wait gate. Only platform containers hold the private mesh credentials;
vLLM serves plain HTTP on port 8000 inside the pod.
The injector forwards the readiness probe through the mesh endpoint to the application's
HTTP path. The rollout wait in Step 2 therefore includes the image's serving-readiness check.
The image may remain blocked until Step 2 admits it. Ordinary outbound TCP is captured by the mesh, so this deployment relies on its bundled model rather than a startup download.
2. Allow the vLLM image
Check CDS against its approved launch pin, current node policy, and operator keys before changing the allowlist:
c8s cds verify "$CDS_URL" --image-policy-file cds-pin.json \
--served-policy-file nodes-policy.json --operator-keys operator.pubRequire exit code 0 and inspect warnings. In an HA deployment, signed Genesis can select a
node set different from the measured configuration's default, and later signed commands can
change it. Use the intended current set in nodes-policy.json.
c8s allowlist add "${VLLM_IMAGE##*@}" "$VLLM_IMAGE" \
--url "$CDS_URL" --image-policy-file cds-pin.json \
--operator-key operator.keyRecord the name after added, then set it for the route:
export WORKLOAD_ENTRY='<PRINTED_ALLOWLIST_ENTRY>'
kubectl rollout status -n workloads deployment/vllm --timeout=10m
kubectl get pods -n workloadsExpect the vLLM pod to become Running with 3/3 ready: vLLM and the two persistent sidecars.
A rollout timeout requires investigating pod events and the image's resource/runtime contract.
It is not an attestation verdict.
The private signing key stays local; a signed token authorizes the write. This convenience entry admits the image with any command, arguments, environment, and mounts, and grants no egress exits. It does not prove the exact model or application configuration. For stricter entries, see Manage the allowlist.
For a future bootstrap, generate the component/workload seed before building cds-config.yaml.
CDS seeds from that measured configuration; changing cluster Helm values does not rewrite the
running external VM's seed.
3. Point the Load Balancer at vLLM
Check the router pods and the Service in their dedicated namespace:
kubectl get pods -n c8s-router
kubectl get svc -n c8s-router c8s-routerRequire ready router pods and the prepared network path. This tutorial keeps the existing front-door configuration: the client reaches the router using its certificate's DNS name and trusts the retained mesh CA for outer HTTPS in Step 6. The same CA separately anchors the SDK's attested mesh identity. Keep TLS certificate validation enabled.
Read the existing route state before choosing whether to create the first manifest:
c8s routes show --url "$CDS_URL" --image-policy-file cds-pin.json \
--operator-keys operator.pubIf this prints a verified manifest, omit --new from both commands below. The specific error
no route manifest stored on CDS means the store is empty: use --new only when creating the
cluster's first manifest. If a CDS reset lost earlier routes, restore the retained approved
manifest before continuing. Stop for any other error.
Preview an exact route for /v1/chat/completions. It takes precedence over prefix routes
for that endpoint and replaces any existing exact route at that host and path. Review that
replacement and every retained route before publishing:
c8s routes add --url "$CDS_URL" --image-policy-file cds-pin.json \
--operator-key operator.key --operator-keys operator.pub \
--host "$CHAT_HOST" --path /v1/chat/completions --match exact \
--address c8s-vllm.workloads.svc.cluster.local:8000 \
--workload "$WORKLOAD_ENTRY" --new --dry-runReview every route in the preview, then publish and read back the signed manifest:
c8s routes add --url "$CDS_URL" --image-policy-file cds-pin.json \
--operator-key operator.key --operator-keys operator.pub \
--host "$CHAT_HOST" --path /v1/chat/completions --match exact \
--address c8s-vllm.workloads.svc.cluster.local:8000 \
--workload "$WORKLOAD_ENTRY" --new
c8s routes show --url "$CDS_URL" --image-policy-file cds-pin.json \
--operator-keys operator.pubConfirm the stored host, exact /v1/chat/completions path, backend, and workload name match
this deployment, then retain the printed digest:
export ROUTE_DIGEST='<ROUTE_DIGEST>'Replace <ROUTE_DIGEST> with the reviewed manifest's 64 lowercase hex characters. This pin is
required in Step 5. The router polls routes every 30 seconds by default.
The route's workload is the allowlist entry name, distinct from the vllm Service ID.
The router authenticates the backend's CDS-issued mesh leaf and its workload stamp, requiring
its allowlist digest to equal the stamped router's. The pod's mesh endpoint decrypts that
connection and delivers ordinary HTTP to vLLM. After an allowlist change, requests can fail
until the router and backend certificates carry matching policy digests.
4. Prepare the client pins
Keep the router identity, CDS identity, and mesh CA separate. Recheck the retained CA through the approved CDS pin without overwriting it:
c8s cds ca --url "$CDS_URL" --image-policy-file cds-pin.json \
--out mesh-ca.candidate.pem
cmp mesh-ca.pem mesh-ca.candidate.pemRequire the CA read to succeed; cmp exit 0 confirms unchanged bytes. The CA command prints
SHA-256 fingerprints of certificate DER on stderr. Investigate any difference through
CDS verification
before accepting a replacement. A singleton restart regenerates its CA and can lose
nonpersistent routes; a surviving HA cluster can restore a restarting replica's state.
Create router-policy.json from the approved router-pin.json obtained through
Azure policy preparation.
For this AKS/AMD SEV-SNP path, copy the router-approved entries' measurements[].measurement
values into the SDK's measurements array. The one-digest example is:
{
"platform": "az-snp",
"measurements": ["<APPROVED_ROUTER_NODE_LAUNCH_DIGEST>"],
"requireCollateral": true
}Replace the placeholder with the approved 96-hex SHA-384 value; include additional approved
router-node digests as separate array entries. Use snp for native SEV-SNP evidence. These
are router-node values, separate from the external CDS policy in cds-pin.json.
Copy the binary DER CRL supplied by the deployment operator:
cp '<OPERATOR_SUPPLIED_CRL_DER>' snp-crl.der<OPERATOR_SUPPLIED_CRL_DER> is the file obtained through the prerequisite's validated
vendor-collateral procedure. Have the operator confirm its processor generation against the
router nodes' deployment inventory and provide a current CRL. Keep the DER bytes unchanged;
Step 5 loads them as a Uint8Array and requires successful collateral verification.
For an existing native Intel TDX deployment, obtain its approved router-node manifest.json
through launch measurement preparation.
Copy mrtd, rtmr1, and rtmr2 from its tdx object (or its top level for a flat manifest)
into this SDK policy. All three 96-character lowercase hex values must come from one build:
{
"platform": "tdx",
"tdxImage": {
"mrtd": "<APPROVED_MRTD>",
"rtmr1": "<APPROVED_RTMR1>",
"rtmr2": "<APPROVED_RTMR2>"
},
"requireCollateral": true
}Supply matching Intel PCS collateral as tdx-collateral.json in the SDK's
tdxCollateral format, using strings for
the verbatim signed JSON bodies and PEM certificate/CRL fields. The script below
supports snp, az-snp, and native tdx; its collateral-required policy does not support
az-tdx. SDK policy JSON is not a CLI --image-policy-file, and binary SNP collateral cannot
be embedded in it as a JSON array.
These reference values come from approved build/deployment records, not from the endpoint being tested. A launch pin establishes only the state that platform measures; a bare AKS digest does not identify every guest or application file. Stock baked nodes leave TDX policy measurement off. For a supported deployment that enables it, prepare the additional history acceptance inputs before connecting.
5. Verify and connect
Create chat.mjs with this block, then append both JavaScript blocks from Step 6 to the same
file. Keep the policy, collateral, and mesh-ca.pem beside it:
import { C8sClient } from "c8s-verify";
import { readFileSync } from "node:fs";
function requiredEnv(name) {
const value = process.env[name];
if (!value) throw new Error(`Set ${name} before running chat.mjs`);
return value;
}
const policy = JSON.parse(readFileSync("router-policy.json", "utf8"));
if (policy.platform === "snp" || policy.platform === "az-snp") {
policy.snpCrl = new Uint8Array(readFileSync("snp-crl.der"));
} else if (policy.platform === "tdx") {
policy.tdxCollateral = JSON.parse(readFileSync("tdx-collateral.json", "utf8"));
} else {
throw new Error("This tutorial requires snp, az-snp, or native tdx evidence");
}
const client = new C8sClient({
...policy,
baseUrl: `https://${requiredEnv("CHAT_HOST")}`,
meshCaPem: readFileSync("mesh-ca.pem", "utf8"),
routeDigests: [requiredEnv("ROUTE_DIGEST")],
requireCollateral: true,
allowPartial: false,
});
const session = await client.connect();
console.error(`Verified router: ${session.attestation.measurement}`);
for (const warning of session.attestation.warnings) console.error(warning);connect() generates a nonce and an X-Wing keypair (X25519 + ML-KEM-768), posts the nonce
and encapsulation key to /.well-known/c8s/attest-pq, and verifies the returned evidence and
transcript. It checks the router policy, mesh CA, route digest, and supplied collateral before
decapsulating and deriving the client channel. Failed checks return no session; do not bypass
them or enable partial results to continue. See
what it verifies.
6. Chat over the sealed channel
Append the request function. The model ID matches the preconfigured image from Step 1:
async function ask(messages) {
const res = await session.fetch("/v1/chat/completions", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ model: "tutorial-model", messages, stream: false }),
});
if (res.status !== 200) throw new Error(`vLLM HTTP ${res.status}: ${res.text()}`);
return JSON.parse(res.text()).choices[0].message.content;
}Append the chat loop:
import { createInterface } from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
const rl = createInterface({ input, output });
const history = [];
try {
for (;;) {
const prompt = await rl.question("you> ");
if (!prompt || prompt === "/exit") break;
const messages = [...history, { role: "user", content: prompt }];
const reply = await ask(messages);
history.push({ role: "user", content: prompt }, { role: "assistant", content: reply });
console.log(`bot> ${reply}\n`);
}
} finally {
rl.close();
}Run from the directory containing the files, with CHAT_HOST and ROUTE_DIGEST
from earlier steps still set:
NODE_EXTRA_CA_CERTS=mesh-ca.pem node chat.mjsExpect Verified router: …, then you>. Send a short prompt; a bot> reply means an
application request and response traversed this verified session. Enter /exit to finish.
If connection or inference fails, resolve the reported failure before sending another prompt.
session.fetch seals the method, path, headers, and body; the router decrypts them, routes to
vLLM, and seals the response. The tunnel buffers a complete response, so this example sets
stream: false. Ordinary HTTPS routes can stream, but their requests do not use this sealed
session. A successful standalone CLI verification does not bind a later ordinary HTTP request.
Route or router policy changes can reject later requests and end the session. The SDK reports
outer non-2xx failures as channel_error; it neither reconnects nor replays requests. Review any
changed pins, then restart the client to establish a fresh session. Checks on a later request
do not cancel an already running exchange.
What you've proven
Your trusted client has checked fresh router evidence against its approved identity, retained mesh CA, and reviewed route digest, then used the resulting channel for a chat request. Channel secrets are derived at the client and attested router. An outer TLS terminator sees sealed application data; the client, router, and application process plaintext.
The router's mesh checks transitively reach the CDS-admitted backend named by your route under operator policy. They authenticate CDS-issued identities, without repeating hardware attestation on every mesh connection. This tutorial's broad image entry does not establish an exact model/configuration or control every application log and storage sink. These checks also do not establish end-to-end GPU attestation.
Where to go next
- Consumer verification explains the trust assumptions.
- c8s-verify reference covers policies, session behavior, and errors.
- Manage the allowlist covers stricter workload entries.
- NVIDIA Dynamo and KServe cover larger serving deployments and their model-delivery prerequisites.