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.

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.

Operator-supplied values

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

<CDS_HOST>:<PORT> is the CDS or the CDS-issued-TLS tls-lb endpoint; <SHA384_LAUNCH_DIGEST> is the launch measurement that endpoint must present.

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; c8s verify prints it (see Verify a workload). --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