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, and mesh-ca.pem.
  • jq and curl on 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:

workload.yaml
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 --strict

Expect 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-nginx

Keep 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 wide

Use --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:

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 --strict

Publish 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.json

Apply 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: true

Save 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 --strict

Immediately 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.json

That 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.