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:
| Annotation | What it does |
|---|---|
confidential.ai/c8s-secrets | Comma-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-dir | Where 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.pemThe 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/dbThe 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.keysandbox 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 setThe 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
- Application secrets — the release decision, the limits, and what a CDS restart destroys.
- Workload annotations — every annotation, and the reserved container and volume names.
- CLI reference →
c8s secrets— every flag. - Create an encrypted volume — for data too large to be a secret.