Serve a model with KServe

Admit a complete KServe predictor, route ordinary HTTP through the C8s mesh, and verify the router before sending a prediction request.

Use this procedure to admit a predictor and connect it to the C8s router after an operator has validated the KServe control plane for the cluster. The predictor serves ordinary HTTP inside its pod. C8s's injected mesh endpoint authenticates incoming TLS and delivers HTTP to the application; the model server needs no mesh certificate or private-key mount.

Prerequisites

An operator must supply the following artifacts for an existing C8s cluster. The file names below are the local names used by this procedure.

InputRequired contents
Control-plane validation bundleDeployed cert-manager/KServe manifests, exact versions and generated pod specifications; effective trusted namespace, node-policy and transport configuration; evidence of controller-to-API-server access, API-server-to-webhook access, and successful predictor reconciliation
iris.yamlApproved Standard-mode InferenceService named iris in the existing, mesh-covered workloads namespace; runtime selection that generates Deployment iris-predictor, HTTP port 8080, and /v1/models/iris:predict accepting the feature-vector request in Step 5
Predictor artifact and validation recordAn operator-supplied immutable image containing an approved non-sensitive Iris model, with the selected runtime configured to load that packaged artifact; model digest, known request/response fixture, and evidence that startup completes without external initializer fetching
iris-env.json, iris-mounts.jsonApproved policies keyed by every application init/main container name, covering effective OCI environments and mount provenance for the generated predictor
cds-pin.json, operator.key, operator.pubIndependently trusted CDS launch-file policy and credentials authorized for the cluster's allowlist and routes
router-pin.json, mesh-ca.pemIndependently trusted router image policy and cluster mesh CA
router-az-snp-measurement.txtFor the Azure SEV-SNP client example, one 96-character hex launch measurement supplied by the trusted cluster operator from the accepted Azure guest build, consistent with router-pin.json

The control-plane bundle must describe both directions of Kubernetes API traffic. Ordinary covered application sockets cannot assume access to the Kubernetes API Service, and ordinary API-server webhook TLS does not satisfy incoming mesh authentication. Any namespace exclusions or privileged platform placement must be part of the cluster's reviewed trusted configuration. An image allowlist, annotation, or workload egress rule does not supply that topology. This page supplies no controller-installation procedure or exemption recipe.

Stop before deployment if the control-plane bundle or packaged-model predictor inputs are unavailable. KServe v0.19.0 and cert-manager v1.20.3 were the versioned inputs to this example; their compatibility, chart fields, and generated resources have not been validated here. The operator must supply the exact tested versions and artifacts. A conventional sequential initializer fetching externally before a distinct required main server starts cannot use that predictor entry's signed egress: the required main policy is not yet admitted. That startup arrangement is excluded from this flow.

You also need c8s, kubectl, and crane; Kubernetes authority to read the validation Deployment and create/update the target workload; the external CDS HTTPS URL; and an already reachable router HTTPS origin whose certificate your client runtime trusts. Install c8s-verify in a runtime supporting its fetch and WebCrypto requirements for the final call. Select the target cluster as your current kubectl context. VALIDATION_CONTEXT below names the operator's validated cluster context.

The cluster's CDS configuration must bind the approved c8s component seed for its exact release and options. Every Node must publish its PodCIDR, outside the CDS inventory node subnets. c8s install refuses fully baked clusters marked confidential.ai/baked=true; their signed launch configuration owns the platform. Chart-managed installations prepare matching inputs with --allowlist-seed-out, then use the paired --cds-address and --cds-pin options. Changed release or router settings can fail that seed check. Cluster installation and router exposure are prerequisites, not steps to repeat here. See Your first confidential cluster.

Step by step

1. Check the supplied predictor shape

The operator's validation record must show that the selected controller reconciles iris.yaml and that the packaged model loads successfully. Capture its generated Deployment:

export VALIDATION_CONTEXT='<OPERATOR_VALIDATED_CONTEXT>'
kubectl --context "$VALIDATION_CONTEXT" get deployment iris-predictor -n workloads \
  -o json > iris-predictor.json

The Deployment's spec.template.metadata.annotations["confidential.ai/cw"] must equal iris, and its generated Pods must inherit that annotation. Validate propagation through the selected KServe API in the supplied record. C8s's workload controller watches the Deployment template and creates headless Service c8s-iris; annotating a standalone Pod does not create that Service. The annotation names Service intent. All covered pods receive the mesh independently of it.

Compare policy-relevant fields with the approved target artifacts: application init/main container membership, platform-specific immutable image digests, resolved commands/arguments, effective OCI environments, and mount rules/provenance. Generated object names, resource versions, and unrelated labels are not the identity policy. Pod JSON is also accepted by derive, but it does not replace the Deployment-template check above.

Review every generated application container, including any local initializer, for:

  • Non-root execution, disabled privilege escalation, allowed capabilities, private process namespaces, and the deployment's admission controls. On baked nodes the measured runtime also enforces its security floor.
  • Supported HTTP health probes with harmless paths, resolvable application ports, no custom headers, and timeouts at most five seconds. TCP/gRPC probes and HTTP lifecycle hooks are refused; exec probes depend on the node's exec mode.
  • Approved storage and local access to the packaged model. Record its content digest and fixture results. This flow does not fetch from an external bucket or grant external model download egress. A public model tests the application path, not secret model delivery.

2. Admit the complete predictor

Use the operator-supplied iris-env.json and iris-mounts.json, each mapping every application init and main container name to its approved policy. Use exact or match environment policies as appropriate, and exact mount rules for reviewed volumes. Derive one named entry:

export CDS_URL='https://<CDS_HOST>:<CDS_PORT>'

c8s allowlist derive iris-predictor iris-predictor.json \
  --env-file iris-env.json \
  --mounts-file iris-mounts.json > iris-entry.json

c8s allowlist lint iris-entry.json --strict
c8s allowlist apply iris-entry.json \
  --url "$CDS_URL" \
  --image-policy-file cds-pin.json \
  --operator-key operator.key

derive emits a bare map with the key iris-predictor, pins argv using the image configuration when needed, and drops C8s-injected containers while naming them on stderr. It does not infer mount provenance, full environment policy, or egress. With no findings, lint prints ok: no findings; a successful apply prints applied iris-predictor. HTTPS commands use the external CDS VM address and its launch-file-bound pin.

CDS matches the complete sandbox admission history to one unambiguous entry, including initializer records retained after exit. Every recorded application container must fit it, and every required main-container policy must appear in that record. This establishes launch identity, not current liveness. Keep conflicting broad entries out of the effective policy.

3. Deploy a confidential InferenceService

Apply the supplied manifest to the target cluster only after the prerequisite checks pass:

kubectl apply -f iris.yaml

Wait for the validated controller to create iris-predictor, then inspect it:

kubectl get deployment -n workloads iris-predictor -o json > iris-target.json
kubectl get inferenceservice -n workloads iris
kubectl get pods -n workloads -l serving.kserve.io/inferenceservice=iris

Compare iris-target.json with the approved fields from Step 1 before continuing. If the controller changes images, argv, environment, mounts, or container membership, stop and have the operator reconcile the artifacts and policy. Inspect admission events if creation fails; a missing Deployment must be resolved through the validated controller topology.

Inspect the generated pod and Service:

kubectl get pod -n workloads -l serving.kserve.io/inferenceservice=iris \
  -o jsonpath='{range .items[0].spec.initContainers[*]}{.name}{"\n"}{end}'
kubectl get svc -n workloads c8s-iris

The injected init-container order begins c8s-mesh, c8s-cert, c8s-cert-wait, followed by any approved application init containers. The Service has clusterIP: None. Application containers cannot mount the platform's private credential volume. The mesh captures application TCP to Service destinations as well as pod IPs; use the headless Service here as an explicit predictor backend.

Proceed only when the supplied packaged-model startup checks and actual readiness checks pass. Kubernetes readiness alone does not prove the predictor's CDS workload stamp.

4. Point the front door at the predictor

Use the router access provisioned with the cluster:

kubectl get svc -n c8s-router c8s-router
export ROUTER_ORIGIN='https://<REACHABLE_ROUTER_HOST>'
export ROUTE_HOST='c8s-router.c8s-router.svc'

For a release named c8s, the default router SAN is c8s-router.c8s-router.svc. Adjust the Service name and routed host for your release or explicit router.san configuration. The external origin and sealed request host can differ. Use the deployment's reachable ingress; port-forwarding into a covered pod is blocked by its mesh rules.

After validating that the predictor listens on HTTP port 8080, store the route:

c8s routes add \
  --url "$CDS_URL" \
  --image-policy-file cds-pin.json \
  --operator-key operator.key \
  --operator-keys operator.pub \
  --host "$ROUTE_HOST" \
  --path / \
  --address c8s-iris.workloads.svc.cluster.local:8080 \
  --workload iris-predictor \
  --new

c8s routes show \
  --url "$CDS_URL" \
  --image-policy-file cds-pin.json \
  --operator-keys operator.pub --raw > iris-routes.json
export ROUTE_DIGEST=$(c8s routes digest iris-routes.json)

Use --new for the cluster's first manifest; omit it when updating an existing one. The command uses prefix matching by default and replaces a route with the same host, path, and match kind. Preserve and review other routes when updating a shared manifest. routes show --raw writes the verified manifest bytes; routes digest prints their SHA-256 hex digest for the client to pin. Review that saved manifest before distributing the pin.

--workload iris-predictor names the complete allowlist entry. It is independent of the iris Service intent. Router authenticates the backend's mesh certificate, requires its named workload stamp, and, for a stamped router, checks equality with its own allowlist digest. The injected endpoint terminates mesh TLS and forwards plaintext to the predictor's HTTP socket. An allowlist update changes the digest, so an already-issued router or backend leaf can refer to the preceding policy until renewal. The credential agent watches CDS policy changes and renews automatically; with the default watch enabled it checks every minute and schedules policy renewal with jitter. An initially unnamed predictor also retries for a named stamp.

Inspect renewal and routing failures with:

kubectl logs -n workloads deployment/iris-predictor -c c8s-cert --since=10m
kubectl logs -n c8s-router deployment/c8s-router -c c8s-cert --since=10m
kubectl logs -n c8s-router deployment/c8s-router -c cds-attest --since=10m

The routing error backend allowlist digest differs from the router's identifies a stamp mismatch. Allow the configured renewal loops to converge, then open a new client session and retry only the non-sensitive fixture in Step 5. If renewal fails or the mismatch persists, stop and give the operator these logs to resolve CDS access, policy matching, or disabled watch settings. Do not mount or manually replace platform credentials in the application.

5. Verify the enclave and predict

Check the router against its trusted image policy and cluster authority:

c8s verify "$ROUTER_ORIGIN" --kind lb --mode attest-pq \
  --image-policy-file router-pin.json \
  --mesh-ca mesh-ca.pem \
  --route-digest "$ROUTE_DIGEST"

attest-pq also works with a WebPKI outer TLS endpoint. Review the verdict, warnings, and policy checks in Verify a deployment. This router check does not execute a prediction or validate the model-storage path.

The following client example is for an Azure SEV-SNP router. It reads the operator-supplied router-az-snp-measurement.txt from the prerequisite bundle; for TDX use the image tuple and platform settings in the client reference. Keep the mesh CA and the reviewed route digest as separate pins.

// predict.mjs
import { C8sClient } from "c8s-verify";
import { readFileSync } from "node:fs";

const client = new C8sClient({
  baseUrl: process.env.ROUTER_ORIGIN,
  platform: "az-snp",
  measurements: [readFileSync("router-az-snp-measurement.txt", "utf8").trim()],
  meshCaPem: readFileSync("mesh-ca.pem", "utf8"),
  routeDigests: [process.env.ROUTE_DIGEST],
});

const session = await client.connect();
const res = await session.fetch("/v1/models/iris:predict", {
  method: "POST",
  headers: {
    "content-type": "application/json",
    host: process.env.ROUTE_HOST,
  },
  body: JSON.stringify({
    instances: [[6.8, 2.8, 4.8, 1.4], [6.0, 3.4, 4.5, 1.6]],
  }),
});
if (res.status < 200 || res.status >= 300) {
  throw new Error(`Predictor returned HTTP ${res.status}: ${res.text()}`);
}
console.log(res.text());
node predict.mjs

The operator-supplied manifest and validation record must use this request path and schema. The program prints the actual response body; its class values depend on the approved model artifact. Compare them with a known test vector from that artifact before using private input. connect() must pass its attestation and pin checks before session.fetch() sends the sealed prediction request.

What you've proven

A successful prediction checks that this deployment's application path works. The client's pins authenticate the router image, cluster mesh authority, and accepted route manifest; the router's backend handshake checks the predictor's named workload identity. Model-content integrity, download confidentiality, storage protection, and correct inference still depend on the reviewed artifact, mounts, delivery path, and runtime. A public iris model is not a validation of secret model loading.

Where to go next