Use application secrets

Give a confidential workload an attestation-gated secret — annotate the pod, wait for the file, supply a value as the operator, replace one, and read c8s secrets explain when the CDS refuses.

c8s releases application secrets under attestation: a pod gets a value only if the images running in its sandbox match an allowlist entry that grants the path. This page is the task. For what the CDS checks, when it will not serve /secrets at all, and why a CDS restart is disruptive, see Application secrets.

The file appears after your container starts. The CDS releases only once every main container in the pod is running, so a consumer that reads its secret path at startup finds nothing there. Wait for the file. This is structural — an init container cannot fix it. See the timing constraint.

The wait is bounded. The injected c8s-secret container makes 60 attempts 5s apart (--attempts, --retry-interval), so a path with no value in the store within roughly five minutes of the pod starting fails the container instead of waiting on it — a stuck release surfaces as a crash-looping c8s-secret, not a pod that hangs forever.

Grant the path first: an entry with no secrets grant releases nothing. See Grant secret paths.

Request the secret in the pod

Annotate the pod alongside confidential.ai/cw:

AnnotationWhat it does
confidential.ai/c8s-secretsComma-separated NAME=/store/path pairs. NAME is the file each value is written to; /store/path is where the value lives in the CDS store.
confidential.ai/c8s-secret-dirWhere the files land. Default /run/c8s/secrets.
apiVersion: v1
kind: Pod
metadata:
  name: api
  annotations:
    confidential.ai/cw: api
    confidential.ai/c8s-secrets: "DB=/tenant-a/db,HF=/tenant-a/hf-token"
spec:
  containers:
    - name: api
      image: example.com/api@sha256:<DIGEST>

confidential.ai/cw is required — the secret annotations without it are rejected at admission, since there is no workload identity to release against.

Each NAME becomes a filename, so it may not contain a path separator and may not be . or .., and two secrets may not share one. Each store path must be absolute, clean, free of a trailing slash, free of percent-encoding, and free of wildcards. A path that is not already canonical is rejected rather than repaired, so the bytes matched against the grant and the bytes used as the store key are the bytes you wrote.

Both are checked at admission, so a malformed pair is a rejected manifest rather than a pod that starts and then fails.

Operator-supplied values

Put the value in the store before you deploy the pod that wants it. The release window above is bounded, and a pod deployed first spends it waiting on you.

c8s secrets put /tenant-a/hf-token \
  --url "https://<CDS_HOST>:<PORT>" \
  --measurements <SHA384_LAUNCH_DIGEST> \
  --operator-key operator.key \
  --mesh-ca mesh-ca.pem < token.txt

<CDS_HOST>:<PORT> is the CDS or the CDS-issued-TLS router endpoint; <SHA384_LAUNCH_DIGEST> is the launch measurement that endpoint must present. --mesh-ca is the PEM bundle of the mesh CA that endpoint must serve — the same anchor c8s verify --mesh-ca takes. The write is refused unless every certificate the CDS serves at /ca is in the bundle; --force writes without the check. A measurement proves the peer is an attested build. The mesh CA identifies your CDS within that build.

The CDS serves the bundle at GET /ca — fetch it from an endpoint a pinned c8s verify has attested, e.g. over a port-forward:

kubectl port-forward -n c8s-system svc/c8s-cds 8443:8443 &
curl -sk https://localhost:8443/ca > mesh-ca.pem

The value is read from stdin or --from-file and stored exactly as read — a trailing newline is part of the value — and the byte count is printed so you can confirm which bytes were sent. --dry-run prints the intended change without calling the CDS.

Writes are authorized by the operator key the CDS already pins for allowlist mutations (--operator-keys), which is the same key the grants themselves are rooted in. The signed token binds the method, the path, and the body, so a captured one cannot be replayed against a different path or a different value.

Replacing a value

A path that already holds a value is refused, and the CLI names what put it there — a workload-generated value or an earlier operator write. --overwrite replaces it and prints what it is replacing before it does:

~ /tenant-a/db (replaces a workload-generated value)
wrote 24 bytes to /tenant-a/db

The store has no versioning and no delete, so a displaced value is gone.

A workload reads its secret into a file once, at startup. A replacement therefore reaches a pod only when that pod next restarts: a Deployment holding the old value keeps it until you roll it. Replacing a path a workload created is worth pausing over for that reason — the pods that generated the value go on using it.

Diagnosing a refusal

A refused pod is told only that it was refused, and the input that decides the matter — what the sandbox is running — is visible only to the CDS. c8s secrets explain is where that is read:

c8s secrets explain --sandbox <SANDBOX_ID> \
  --url "https://<CDS_HOST>:<PORT>" \
  --measurements <SHA384_LAUNCH_DIGEST> \
  --operator-key operator.key
sandbox    0123456789abcdef…
inventory  10.0.0.7
reported   3 container(s)
dropped    1 injected by c8s
candidates 2
    sha256:1111…  [/serve]
  - sha256:9999…  [get-secret]
    sha256:8888…  [sh -c sleep 1]

vllm-llama  NEAR MISS
  foreign  sha256:8888…  [sh -c sleep 1]
           no container in this entry declares it

nothing is released: no entry describes the candidate set

The report is laid out in the order the CDS decides, so the first thing that goes wrong is the first thing you read. It uses the same inventory, binding, and allowlist the release path uses, and measures entries with the same matcher — it reports the decision rather than a reconstruction of it.

<SANDBOX_ID> is on the pod's certificate. A workload that serves RA-TLS gives it up to c8s verify (see Verify a workload). One that serves nothing — a batch job, or a pod already crash-looping, which is when you usually need this — has no endpoint to dial, so read the ID off the injected certificate instead:

kubectl exec <POD> -c <CONTAINER> -- cat /etc/c8s/certs/tls.crt > workload.crt
openssl x509 -in workload.crt -noout -text | grep -A1 '1.3.6.1.4.1.66378.1.4'

1.3.6.1.4.1.66378.1.4 is the pod-sandbox-ID extension, and the 64 hex characters it carries are the ID. The certificate is world-readable inside the pod, so any container in it will do.

--json emits the report as it arrives. It answers to the operator key, since it describes a pod the caller may not own. The report carries grant paths; a value never appears in it.

See also