Run a confidential workload
Admit an HTTP application, route to its private mesh endpoint, and send a request through a verified encrypted session.
Run your application on an ordinary HTTP port. c8s injects the private mesh endpoint,
and the router authenticates its Certificate Distribution Service (CDS)-issued identity before
forwarding a request. The confidential.ai/cw annotation supplies a stable Service name;
every pod covered by the webhook receives mesh injection.
Prerequisites
- An installed deployment, a namespace
covered by its webhook,
and
kubectlaccess to create workloads there. c8s,kubectl, andcranelocally, with registry access for the application image.- A digest-pinned application image whose default command runs as a numeric non-root user,
listens on
0.0.0.0:8080, needs no volume mounts, and serves a harmlessGET /app-health. Adapt the manifest and derive its policy again if your application needs different inputs. - A reachable direct CDS HTTPS URL, its approved
cds-pin.jsonimage/launch-file policy, an authorizedoperator.key, and the trusted public-key bundleoperator.pub. - The independently approved router node policy saved as
router-pin.json, the intended current node policy asnodes-policy.json, and a pinned clustermesh-ca.pem. See Verify a deployment for obtaining and checking these anchors. A CA advertised by the target alone is not an independent cluster pin. - The SDK policy
router-client-policy.json, prepared in the request step. Its concrete examples use a baked router node's trusted image manifest and exact launch-bundlepubkey, saved locally asrouter-pubkey.pem. Obtain these through your approved build and deployment records; the node launch contract identifies the files and launch bindings. Managed-platform clients need their own SDK platform policy. - A reachable router HTTPS hostname with working client-trusted TLS, attestation enabled,
and a built
c8s-verifypackage in a Node application. Follow the JavaScript prerequisites and installation; the request below needs Node 20.19 or later and the package's WASM assets.
Use one local working directory for the manifests, trusted files, and request script below.
Prepare the application
Check namespace coverage before choosing <NAMESPACE>:
kubectl get mutatingwebhookconfiguration <RELEASE>-pod-injector -o yaml
kubectl get namespace <NAMESPACE> --show-labelsReplace <RELEASE> with the Helm release name (c8s by default). Compare the namespace's
labels with pods.c8s.confidential.ai's namespaceSelector; every expression must match.
In the chart's selector, kubernetes.io/metadata.name must be outside the excluded names.
Choose an existing namespace that matches, and use it throughout this guide.
Save this as deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-inference
namespace: <NAMESPACE>
spec:
selector:
matchLabels:
app: my-inference
template:
metadata:
labels:
app: my-inference
annotations:
confidential.ai/cw: my-inference
spec:
enableServiceLinks: false
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: server
image: <IMAGE_REPOSITORY>@sha256:<DIGEST>
ports:
- name: http
containerPort: 8080
readinessProbe:
httpGet:
path: /app-health
port: http
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]Replace <NAMESPACE> with the covered namespace, <IMAGE_REPOSITORY> with your registry
repository, and <DIGEST> with its 64-character SHA-256 manifest digest.
The image's default command must actually start the server; containerPort only declares
the port. Keep the application free of mesh certificate mounts.
The webhook rewrites this HTTP readiness probe through the mesh health listener, so rollout
readiness waits for the application's harmless endpoint to respond successfully.
The operator creates the headless Service c8s-my-inference and requests the certificate
DNS name c8s-my-inference.<NAMESPACE>.svc. Use a DNS-label-safe annotation value.
Admit the complete application
kubectl create --dry-run=client -o json -f deployment.yaml > deployment.json
c8s allowlist derive inference-policy deployment.json \
--env any --mounts deny > entry.json
c8s allowlist lint entry.json
c8s allowlist apply entry.json \
--url https://<CDS_HOST>:<CDS_PORT> \
--image-policy-file cds-pin.json \
--operator-key operator.keyReplace <CDS_HOST> and <CDS_PORT> with the directly reachable CDS endpoint. The derive
command reads the image's baked command through crane. It includes every application init
and main container and excludes injected platform containers.
Here, --env any leaves the environment unconstrained; use an explicit environment policy
when those values affect trust. --mounts deny admits platform mounts alone, which suits this
no-volume application. See Manage the allowlist
for exact environment and data-mount rules.
inference-policy is the complete allowlist entry name, distinct from the annotation's
Service ID my-inference. CDS stamps the matched entry name into the mesh leaf using the
sandbox's retained admission records, including exited containers. A matching stamp proves
admission under that entry, not current container liveness.
Start the workload and check its Service
kubectl apply -f deployment.yaml
kubectl rollout status deployment/my-inference -n <NAMESPACE> --timeout=5m
kubectl get service c8s-my-inference -n <NAMESPACE>
kubectl get endpointslices -n <NAMESPACE> \
-l kubernetes.io/service-name=c8s-my-inferenceConfirm the rollout completes, the Service has CLUSTER-IP None, and its EndpointSlices
contain ready pod addresses. A ready pod alone does not prove the router can authenticate
its mesh leaf; the verified request below checks the routed path.
Check the router's public endpoint
kubectl get deployments,services,pods -n c8s-routerUse your deployment's ingress or node host port to reach the router on HTTPS. Configure DNS,
firewalls, and the public certificate for <ROUTER_HOST>, the lowercase hostname used below
without a port. An internal name such as <RELEASE>-router.c8s-router.svc is not a public
endpoint. Pod port-forwarding is not a substitute for this network path.
For chart-managed routing, router.san selects nginx hostnames and its first entry selects
the CDS-issued certificate name. router.publicTLS.mode selects cds, webpki, or acme;
a WebPKI TLS Secret belongs in c8s-router. Baked nodes take public hostnames and TLS selection
from the signed launch configuration. See the router values.
Keep router.attest.enabled enabled and expose /.well-known/c8s/ through the ingress.
The final request uses attest-pq, which encrypts application data to the attested router
inside the Trusted Execution Environment (TEE), including with a WebPKI front door.
WebPKI mode refuses the ordinary-TLS attest-lb mode.
Sign the route
Check for an existing signed manifest:
c8s routes show \
--url https://<CDS_HOST>:<CDS_PORT> \
--image-policy-file cds-pin.json --operator-keys operator.pubIf CDS reports no stored manifest, check your deployment records: absence alone cannot
distinguish first initialization from a CDS reset. Add --new to the next command only when
those records confirm this is the cluster's first manifest. After a reset, restore the last
saved complete manifest with c8s routes put before adding this route. Resolve other errors
before continuing.
In the displayed manifest, check for an exact route at /app-health on <ROUTER_HOST>.
The next command replaces that entry if it exists; confirm that its traffic belongs to
inference-policy before writing. An exact route takes precedence over prefix routes for
the final request.
c8s routes add \
--url https://<CDS_HOST>:<CDS_PORT> \
--image-policy-file cds-pin.json \
--operator-key operator.key --operator-keys operator.pub \
--host <ROUTER_HOST> --path /app-health --match exact \
--address c8s-my-inference.<NAMESPACE>.svc:8080 \
--workload inference-policy
c8s routes show \
--url https://<CDS_HOST>:<CDS_PORT> \
--image-policy-file cds-pin.json --operator-keys operator.pub \
--raw > routes.json
c8s routes digest routes.json > route-digest.txtThe command verifies the stored signature and preserves other routes. Review the exact
/app-health route's host and backend in routes.json
before accepting its digest as the client route pin.
The router authenticates the backend as inference-policy. When the router carries a workload
stamp, it also requires the backend's allowlist digest to match its own. Traffic reaches port
8080 through the injected mesh; the application receives HTTP.
Verify the anchors and route
Wait at least one configured router.routing.pollInterval after the write, then verify the
new digest. The default is 30s; adjust the wait below if your interval is longer.
sleep 30
c8s verify https://<CDS_HOST>:<CDS_PORT> --kind cds \
--image-policy-file cds-pin.json --operator-keys operator.pub \
--served-policy-file nodes-policy.json
c8s verify https://<ROUTER_HOST> --kind lb --mode attest-pq \
--image-policy-file router-pin.json --mesh-ca mesh-ca.pem \
--route-digest "$(cat route-digest.txt)"Require exit code 0; investigate a partial result (4) before sending application data.
Successful router verification confirms that the reached replica attests the new route digest.
If a route-digest mismatch persists after polling, inspect that router pod's cds-attest logs
for route-fetch or signature failures before retrying.
nodes-policy.json must describe the independently intended current node set, including any
signed changes since initialization. The router uses its own approved policy, not the CDS pin.
These checks verify their own exchanges. The application client must verify its own session.
Send a request through the verified session
Create router-client-policy.json using one of these baked-node examples. For AMD SEV-SNP:
{
"platform": "snp",
"measurements": ["<ROUTER_SHA384_LAUNCH_DIGEST>"]
}Use snp_variants[].measurement.snp_launch_digest for the deployed vCPU count in the trusted
node manifest.json; see supported vCPU counts.
The script below derives initDataHash as SHA-256 of the exact router-pubkey.pem bytes,
matching the baked node's HOST_DATA launch binding.
For Intel TDX, use this complete image-policy shape instead:
{
"platform": "tdx",
"tdxImage": {
"mrtd": "<MRTD>",
"rtmr1": "<RTMR1>",
"rtmr2": "<RTMR2>"
}
}Copy the three 96-character hex values from the trusted manifest's tdx object. The script
sets operatorPublicKeyPem from router-pubkey.pem for the TDX RTMR[3] launch binding;
it sets neither measurements nor initDataHash for this TDX example.
Add required TCB and policy-history constraints using
client configuration. For SNP revocation
checking, obtain the AMD KDS CRL for the node's processor generation as binary DER
amd-crl.der and set requireCollateral: true in the JSON. The script loads its bytes as a
Uint8Array; binary snpCrl does not belong in JSON. TDX uses the documented tdxCollateral
object. The SDK policy is a separate format from the CLI's router-pin.json.
Save this as request.mjs in your Node application:
import { readFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import { C8sClient } from "c8s-verify";
const read = (path) => readFile(path, "utf8");
const policy = JSON.parse(await read("router-client-policy.json"));
const pubkey = await readFile("router-pubkey.pem");
if (policy.platform === "snp") {
policy.initDataHash = createHash("sha256").update(pubkey).digest("hex");
if (policy.requireCollateral) policy.snpCrl = await readFile("amd-crl.der");
} else if (policy.platform === "tdx") {
policy.operatorPublicKeyPem = pubkey.toString("utf8");
} else {
throw new Error("This example requires a baked SNP or TDX node policy");
}
const client = new C8sClient({
...policy,
baseUrl: "https://<ROUTER_HOST>",
meshCaPem: await read("mesh-ca.pem"),
routeDigests: [(await read("route-digest.txt")).trim()],
allowPartial: false,
});
const session = await client.connect();
const response = await session.fetch("/app-health");
if (response.status !== 200) {
throw new Error(`Application returned HTTP ${response.status}`);
}
console.log(response.status, response.text());Replace <ROUTER_HOST> with the same public hostname used in the signed route, then run:
node request.mjsSuccess is a verified session followed by the application's expected 200 response and body.
A route or router identity change can invalidate the session; reconnect and verify again.
The SDK reports non-success tunnel HTTP responses as channel_error and does not transparently
replay your request. Already running streams are not stopped by these admission checks.
What the webhook adds
The first three injected init containers are c8s-mesh, c8s-cert, and c8s-cert-wait.
The mesh endpoint and certificate client run as sidecars; the wait container gates application
startup on the first certificate. Only injected platform containers mount /etc/c8s/certs.
Application certificate mounts and credential-path overrides are rejected.
See Workload annotations for reserved names and probe restrictions. Add application secrets or encrypted volumes with their documented annotations and corresponding allowlist rules.
Confirm it worked
If startup or the request fails, inspect the pod and its events:
kubectl get pods -n <NAMESPACE> -l app=my-inference
kubectl get pod <POD> -n <NAMESPACE> \
-o jsonpath='{range .spec.initContainers[*]}{.name}{"\n"}{end}'
kubectl describe pod <POD> -n <NAMESPACE>
kubectl logs <POD> -n <NAMESPACE> -c c8s-certReplace <POD> with the pod name from the first command. An allowlist miss can prevent runtime
container creation after the injected containers already appear in the pod spec. A 502 can
indicate backend mesh authentication failure, including an absent named stamp or mismatched
allowlist digests while certificates renew. See Troubleshoot an install.