Run a workload
Approve a digest-pinned container and its launch constraints, deploy it with kubectl, and route a test request through the built-in router.
Approve a workload in CDS before deploying it. The worker checks image digests and launch constraints; the injected mesh obtains the admitted workload's identity. Your ordinary kubeconfig creates the Kubernetes objects.
Before you start
Complete Start a worker. You need:
- Its shell and
KUBECONFIG. operator.key,operator.pub,cds/cds-pin.json, andmesh-ca.pem.jqandcurlon your operator machine.
Disable Istio sidecar injection in the default namespace used by this example.
This example runs nginx on port 8080, using the source-pinned
1.30.6-alpine-slim image. Approve that image for your own deployment.
It serves public test content; complete client verification before sending
private data.
Write the manifest and derive its entry
Save this Deployment as workload.yaml. Its pinned startup script writes an
nginx configuration with an unprivileged listener, PID, and temporary files
under /tmp:
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-nginx
namespace: default
spec:
replicas: 1
selector:
matchLabels: {app: demo-nginx}
template:
metadata:
labels: {app: demo-nginx}
annotations:
confidential.ai/cw: demo-nginx
spec:
enableServiceLinks: false
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 101
seccompProfile: {type: RuntimeDefault}
containers:
- name: app
image: nginxinc/nginx-unprivileged@sha256:5d8bb9437d74a600ec5dbf90c035451c724dc33eac8d3a71078f6f8612c345f6
command: ["/bin/sh", "-ec"]
args:
- |
cat > /tmp/nginx.conf <<'NGINX'
pid /tmp/nginx.pid;
error_log /dev/stderr;
events {}
http {
access_log /dev/stdout;
client_body_temp_path /tmp/client_temp;
proxy_temp_path /tmp/proxy_temp;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
server { listen 8080; root /usr/share/nginx/html; }
}
NGINX
exec /usr/sbin/nginx -c /tmp/nginx.conf -g 'daemon off;'
ports:
- {name: http, containerPort: 8080}
securityContext:
allowPrivilegeEscalation: false
capabilities: {drop: ["ALL"]}Convert the manifest to JSON and derive its policy entry:
kubectl create --dry-run=client -f workload.yaml -o json > workload.json
c8s allowlist derive demo-nginx workload.json \
--env any --mounts deny > entry.json
c8s allowlist lint entry.json --strictExpect ok: no findings. confidential.ai/cw names the policy entry on the
pod template.
Explicit argv pins the startup script without a registry config lookup. Its writable paths use containerd's memory-backed storage, or the optional encrypted container cache disk.
derive takes JSON and includes authored init and main containers. It
removes c8s-injected containers when reading an admitted object. It does not
derive host-port grants or arbitrary mount policies from the pod spec.
Here env:any accepts any environment and mounts:deny admits only platform
mounts. This basic workload has no secret, capability, direct-egress, or
host-port grant. Pin environment values that affect your application's behavior
before relying on them as part of its identity.
Publish and deploy
c8s allowlist apply entry.json \
--url https://cds.example.com:8443 --image-policy-file cds/cds-pin.json \
--operator-key operator.key
c8s allowlist export allowlist-basic.json \
--url https://cds.example.com:8443 --image-policy-file cds/cds-pin.json
c8s allowlist get demo-nginx \
--url https://cds.example.com:8443 --image-policy-file cds/cds-pin.json \
> served-entry.json
jq -e --slurpfile desired entry.json \
'. == $desired[0]["demo-nginx"]' served-entry.json
kubectl apply -f workload.json
kubectl -n default rollout status deployment/demo-nginx --timeout=5m
kubectl -n default get pods -l app=demo-nginx -o wide
kubectl -n default get service c8s-demo-nginxKeep every full canonical policy archive for later approval and history pins; use a distinct filename immediately after each publication, before the next update. Require these results:
- The comparison prints
true. - The rollout completes.
- The operator creates the headless Service
c8s-demo-nginx, which resolves directly to the workload's pod IPs for mesh routing.
Kubernetes readiness includes the injected certificate gate; an adversarial control plane's status is not independent attestation evidence.
For a rejected launch, inspect kubectl -n default describe pod and events.
Compare the served entry with:
- The actual image and complete argv.
- The environment and mounts.
- All init containers.
Preserve the runtime and mesh restrictions while correcting the policy. Re-derive and publish the entry when those launch inputs change.
Add ingress and test a response
The built-in router already has measured launch constraints and its platform host-port grants. Use its signed route manifest for ingress:
c8s routes add --new --host api.example.com --path / \
--workload demo-nginx \
--address c8s-demo-nginx.default.svc.cluster.local:8080 \
--operator-keys operator.pub --operator-key operator.key \
--url https://cds.example.com:8443 --image-policy-file cds/cds-pin.json
c8s routes show --operator-keys operator.pub \
--url https://cds.example.com:8443 --image-policy-file cds/cds-pin.json
kubectl -n c8s-router get pods -o wideUse --new for the cluster's first route manifest. Subsequent edits omit it.
Expect the route output to show its signature verified against operator.pub
and the demo-nginx backend. Routers poll for updates every 30 seconds.
The initial manifest expires after 24 hours; renew it under your route
operations procedure before its validity window ends.
Set ROUTER_IP to the primary worker address hosting a ready router pod:
ROUTER_IP='<ROUTER_WORKER_IPV4>'
curl --fail --show-error --connect-timeout 5 --max-time 15 \
--retry 40 --retry-delay 3 --retry-max-time 180 \
--cacert mesh-ca.pem --resolve "api.example.com:443:$ROUTER_IP" \
https://api.example.com/The router must apply the route and renew its policy-stamped credential. The GET retries transient errors; exhausted retries fail and need investigation. Expect the nginx welcome page.
This deployment's launch document selects a CDS-signed router certificate,
trusted here through the previously authenticated mesh-ca.pem.
The router verifies the backend's workload identity and checks its policy against the router's, including supported policy-history transitions.
This request checks service connectivity and CA trust; use client image, policy, and route pins before sending private requests.
A separate ingress controller needs its own approved digest, launch constraints, mounts, privileges, and host-port mappings. Ordinary TCP to the workload goes through its mesh; do not disable capture to make an ingress controller work.
Grant a host port when needed
For an application that needs a worker-address port, add a mapping to both
the pod and its entry. This optional variant publishes TCP 9443 to nginx's
8080. It keeps the same entry name and replaces the basic policy above.
Every init and main container in an entry with host ports, capabilities, or
direct egress must pin command, args, mounts, and its complete environment.
env:any fails those grants. For this image and non-interactive pod, the
following map covers image defaults, containerd's hostname, and Kubernetes'
API Service variables with service links disabled.
Save this map as env-policy.json:
{"app":{"policy":"match","variables":{
"PATH":{"exact":"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"},
"NGINX_VERSION":{"exact":"1.30.6"},
"PKG_RELEASE":{"exact":"1"}, "DYNPKG_RELEASE":{"exact":"1"},
"HOSTNAME":{"present":true},
"KUBERNETES_SERVICE_HOST":{"present":true},
"KUBERNETES_SERVICE_PORT":{"exact":"443"},
"KUBERNETES_SERVICE_PORT_HTTPS":{"exact":"443"},
"KUBERNETES_PORT":{"present":true},
"KUBERNETES_PORT_443_TCP":{"present":true},
"KUBERNETES_PORT_443_TCP_ADDR":{"present":true},
"KUBERNETES_PORT_443_TCP_PORT":{"exact":"443"},
"KUBERNETES_PORT_443_TCP_PROTO":{"exact":"tcp"}
}}}Add the port to the manifest and derive the constrained entry:
jq '.spec.template.spec.containers[0].ports[0] +=
{"protocol":"TCP","hostPort":9443}' workload.json > workload-hostport.json
c8s allowlist derive demo-nginx workload-hostport.json \
--env-file env-policy.json --mounts deny > entry-hostport.json
jq '.["demo-nginx"].hostPorts =
[{"protocol":"TCP","hostPort":9443,"containerPort":8080}]' \
entry-hostport.json > entry.json
c8s allowlist lint entry.json --strictPublish entry.json with the earlier allowlist apply command, then immediately archive:
c8s allowlist export allowlist-hostport.json \
--url https://cds.example.com:8443 --image-policy-file cds/cds-pin.jsonApply workload-hostport.json and wait for its rollout. The mapping permits the
publication; it retains mesh capture and does not create a plaintext ingress
bypass. External clients use the router or an attested mesh client.
For another image, review its complete OCI environment, including runtime
additions; a match policy refuses extra or missing variable names. Pin loader
variables such as PATH exactly. Reserved worker ports include 1019,
2379, 2380, 6443, 8443, 9345, and 10250.
Author mount rules explicitly
For public weights on a worker's optional confai-models disk, a container
mount and its policy can be paired as follows. Add the volume under the pod
spec and the mount under its app container:
volumes:
- name: models
hostPath: {path: /var/lib/models, type: Directory}volumeMounts:
- name: models
mountPath: /models
readOnly: trueSave this as mount-policy.json. Create the constrained env-policy.json
from the preceding section even if you do not publish a host port: host mounts
require a pinned launch, including a constrained complete environment:
{"app":{"policy":"exact","rules":[
{"destination":"/models","kind":"host","source":"/var/lib/models","readOnly":true}
]}}After editing workload.yaml, regenerate the JSON and derive with both policy
files, then publish the entry with the earlier allowlist apply command:
kubectl create --dry-run=client -f workload.yaml -o json > workload.json
c8s allowlist derive demo-nginx workload.json \
--env-file env-policy.json --mounts-file mount-policy.json > entry.json
c8s allowlist lint entry.json --strictImmediately after publishing, archive the full policy before applying the manifest:
c8s allowlist export allowlist-mounts.json \
--url https://cds.example.com:8443 --image-policy-file cds/cds-pin.json
kubectl apply -f workload.jsonThat source is a path inside the worker guest. Attach the models disk
read-only through your substrate; its contents are host-supplied. Read-only
access does not authenticate them: the application must verify approved content
digests. A nodeRootFile rule instead requires a read-only regular file from
the measured immutable root. Ordinary host mounts prevent secret grants;
read-only nodeRootFile rules can accompany them.
Policies cover every authored init and main container. Use memory or encrypted pod storage for private data; follow Secrets and Encrypted volumes for those grants.