Confidential sandboxes: attestation, not credentials¶
The blueprints in this section use runtimeClassName: confidential-sandbox, which treats the
node as untrusted. Each one gives a workload something only the sandbox should be able to
read, and makes AWS KMS, outside the cluster, decide whether to release the key.
This page is the part they share: the trust boundary, the shape of the KMS key policy, the
attested decrypt, how a blueprint proves it ran in a sandbox, and what the measurement does
and does not bind. Each blueprint's page links here instead of restating it, and reuses the
templates in _shared/.
The trust boundary¶
In confidential mode the sandbox is the security boundary, not the node. The Nitro
hypervisor keeps the parent instance from reading sandbox memory or CPU state, from forging
attestation documents, and from decrypting encrypted image layers; the container's writable
layer and its emptyDir volumes sit on an encrypted overlay inside the sandbox (Amazon EKS
User Guide, Security model and parent instance trust assumptions).
The figure shows how the pieces on this page fit together. The node sits outside the
boundary, and it can use the pod's Pod Identity credentials. Inside the sandbox, the agent
measures the pod's Cedar policy into PCR17, and a guest caller asks the Nitro Secure
Module for an attestation document carrying a public key of its own. The guest caller is
the workload in the two application blueprints, and the guest image runtime in
encrypted-image. AWS KMS, outside the cluster, allows
kms:Decrypt to the pod's role only when the attestation also matches the key policy, and
it returns the key wrapped to the guest's public key, so only the guest can unwrap it. The
node can send the same request with the same credentials, but it has no matching
attestation to present, and is refused.
Figure 1: How a confidential pod gets a key. The credentials alone are not enough: KMS also requires a matching attestation, and returns the key wrapped so that only the guest can open it.

Two consequences shape every blueprint here.
- Credentials are not a secret. The parent instance can obtain the same EKS Pod
Identity credentials the workload uses, either by reading them as they are delivered or
by minting them from the kubelet's own Kubernetes credentials and the node role. IAM
conditions such as
aws:SourceVpc, principal tags and session tags do not tell the sandbox apart from the parent instance (Amazon EKS User Guide, Security model and parent instance trust assumptions). So a blueprint never relies on the pod's credentials staying secret: it assumes anything the role permits is reachable from outside the sandbox, and gives the role nothing but a conditional grant. - Measurement without a verifier decides nothing. The sandbox measures the pod's Cedar policy into a register, but nothing inside the sandbox or on the node checks that measurement against an expected value. A cluster operator can write a different policy, attach it, and the sandbox will accept and enforce it: the attestation changes and nothing rejects the pod. The loop closes only when something outside checks the attestation before releasing a secret, which is what the KMS key policy below does (Amazon EKS User Guide, Security model and parent instance trust assumptions, and Cedar policies).
The workload is outside the boundary. Confidential mode protects the workload's data from the node. It does not protect that data from the workload. Anything the container chooses to do with its plaintext, writing it to stdout, to a shared volume, or to a network destination of its own choosing, is outside what the runtime can prevent. Auditing the image and the application code is the workload owner's job (Amazon EKS User Guide, Security model and parent instance trust assumptions).
The policy and its measurement¶
A confidential-sandbox pod needs a Cedar policy. Unlike isolated-sandbox, which applies
a permissive built-in default, confidential mode enforces a strict default-deny posture, so
a pod with no policy does not run. The policy generator the Amazon EKS User Guide names,
eks-nitro-sandbox-genpolicy, reads the pod manifest and emits the policy; it travels to
the guest as the sandbox.amazonaws.com/policy annotation, a base64-encoded, gzipped JSON
envelope (Amazon EKS User Guide, Cedar policies).
The generator also prints the measurement the sandbox will extend that policy into, which is what the key policy is set to expect. The Amazon EKS User Guide names two registers:
| Register | What it measures |
|---|---|
PCR17 |
The policy. The agent hashes the uncompressed envelope JSON with SHA-384 and extends the result into PCR17, so the generator can predict it before any pod runs. Change any pinned value in the pod spec and the policy changes, and so does this. |
PCR8 |
The certificate that signed the sandbox environment image. Always condition on kms:RecipientAttestation:PCR8:ServiceName, never a raw PCR8 value: AWS rotates the signing certificate, which changes the raw value, while the service-name condition stays valid across rotations. |
Source for both, and for the generator's predicted value: Amazon EKS User Guide, Cedar policies, and Security model and parent instance trust assumptions.
The cedar-policy blueprint under Policy is the place to start if you have not generated a policy before. It shows the generation, what the policy pins, and the guest enforcing it, and stops short of attestation.
The KMS key policy¶
Every blueprint here puts the same shape on its key, in
_shared/kms-key-policy.json: allow on a matching
attestation, explicitly deny otherwise.
- Key administration lists its actions rather than using
kms:*, and leaves outkms:Decryptandkms:ReEncryptFrom.ReEncryptFrommatters as much asDecrypt, because it would let a key administrator re-wrap the protected key under a second, ungated key and decrypt it there. - One conditional allow for
kms:Decrypt, to the pod's dedicated role, when the attestedPCR17is the approved policy's measurement andPCR8:ServiceNameiseks.amazonaws.com. - Two denies, on every principal, when either of those does not match. A negated
condition operator is also true when the condition key is absent from the request, so a
Decryptcarrying no attestation document at all is caught by both.
The condition keys are the AWS KMS Nitro Enclaves ones:
kms:RecipientAttestation:PCR<PCR_ID>
and
cryptographic attestation support in AWS KMS.
The administration statement replaces the
default key policy's
broad "Enable IAM policies" statement, which would otherwise let any sufficiently
privileged identity in the account decrypt.
The role carries no identity policy. Each blueprint gives the pod a dedicated role with
nothing attached or inline, and scopes its trust policy to one cluster, namespace and
service account with the aws:RequestTag conditions EKS Pod Identity sets
(_shared/pod-identity/role-trust-policy.json).
Everything the role may do is in the key policy, where the attestation condition is, so
there is no second path to kms:Decrypt that skips the gate. The setup scripts check for
identity policies on the role and stop if they find one.
What these blueprints pin. Exactly two conditions, which are the ones the policy generator prints for you to paste (Amazon EKS User Guide, Cedar policies):
"Condition": {
"StringEqualsIgnoreCase": {
"kms:RecipientAttestation:PCR17": "<predicted PCR17>",
"kms:RecipientAttestation:PCR8:ServiceName": "eks.amazonaws.com"
}
}
Several kms:RecipientAttestation:* keys in one StringEqualsIgnoreCase block are
combined with AND, so a key policy can pin more registers than this, and every one listed
then has to match before Decrypt succeeds. These blueprints pin no more than the two above,
so that what they install is exactly what the generator emitted, with nothing added by
hand.
On the deny side, give each register its own statement. The keys in a
StringNotEqualsIgnoreCase block are ANDed too, so one deny listing two registers fires
only when both fail to match, which is far weaker than it looks: a request matching one
register and not the other slips past it. That is why the template below carries two
separate denies, one per register, rather than one with both. If you add a register, add a
deny for it; the setup script's gate check reads the live policy back and refuses to go on
if it finds the merged form.
What each condition binds. These two are the minimum the Amazon EKS User Guide, Security model and parent instance trust assumptions, asks for on a KMS key that protects confidential workload data, and the guide describes them as what enforces the integrity of the Cedar policy and the system image.
PCR17is the policy hash. It binds key release to one exact policy document, and so to the image digest, environment, identity and mounts that policy pins. A changed policy gives a differentPCR17, and KMS refuses to decrypt.PCR8:ServiceNameis the condition on the system image signing certificate. Use it rather than a rawPCR8value, so that the condition stays valid when AWS rotates that certificate.
The attested decrypt¶
A caller inside the guest does not ask KMS for plaintext. It asks the Nitro Secure Module
for a signed attestation document that carries an ephemeral public key, and passes that
document to kms:Decrypt as the Recipient. KMS validates the document, checks its
measurements against the key policy's conditions, and returns the plaintext encrypted to
that public key in
CiphertextForRecipient
instead of returning plaintext. Only the matching private key, which never leaves the
guest, can unwrap it.
attestation = NSM.attest(ephemeral_public_key)
response = KMS.Decrypt(CiphertextBlob = wrapped_key, Recipient = attestation)
plaintext = unwrap(response.CiphertextForRecipient, ephemeral_private_key)
Which component does this differs by blueprint. In encrypted-image the caller is the guest's own image runtime, before the container starts, and the workload image contains no KMS code at all. In the two application blueprints the workload makes the call itself.
An attestation document names the instance. It includes a module identifier of the form
i-<instance-id>-enc<enclave-id>, so anyone who receives one can tell which EC2 instance
the sandbox is running on. Do not treat placement as confidential if you share attestation
documents with third parties (Amazon EKS User Guide, Security model and parent instance
trust assumptions).
How these blueprints prove a pod ran in a sandbox¶
The generated confidential policy denies exec, so the usual way of reading a pod from the
outside is not available, by design. Every blueprint here has its container print its own
readings at start-up and reads them with kubectl logs, which the policy permits, and runs
an ordinary pod on the same node for comparison:
| Reading | In the sandbox | In an ordinary pod on the same node |
|---|---|---|
uname -rv |
The guest's own kernel build | The node's kernel build |
cat /proc/uptime |
Seconds, matching the pod's age, because the VM booted with the pod | The node's uptime |
ls /sys/block |
Virtio devices (vda, vdb), plus the dm- and loop devices of the encrypted overlay |
The instance's nvme devices |
cat /sys/class/dmi/id/product_name |
The VM in the sandbox | The EC2 instance type |
The kernel version can be the same in both; it is the build and the boot that differ. The runtime class on the pod shows what was asked for, not what ran, so it supports the claim without proving it. The hello sandbox page introduces these readings.
The shared files¶
| File | What it is |
|---|---|
_shared/confidential-profile.yaml |
The generator profile the blueprints pass to --profile: the confidential posture, reduced to the fields the current preview guest's policy schema defines |
_shared/kms-key-policy.json |
The key policy template described above, with placeholders a setup script fills in |
_shared/pod-identity/role-trust-policy.json |
The Pod Identity role trust policy, scoped to one cluster, namespace and service account |
_shared/attested_recipient.py |
The attested decrypt above, as Python: the ephemeral key, the attestation document from the Nitro Secure Module, and the unwrap of CiphertextForRecipient. Used by the blueprints whose workload calls KMS itself |
Each template keeps its explanation in a _comment key, which a setup script drops before
passing the document to AWS.
The blueprints¶
- encrypted-image: an image whose every layer is ciphertext in Amazon ECR, decrypted inside the sandbox by the guest image runtime after KMS checks the attestation. The workload image carries no decryption code.
- secrets-manager-secret: a secret stored in AWS Secrets Manager as a KMS ciphertext envelope, read and decrypted by the application itself inside the guest. An ordinary pod holding the same role can read the envelope and is refused the value.
- s3-encrypted-file: a file encrypted with AES-256-GCM before it is uploaded to Amazon S3, with its data key wrapped by KMS. The application in the sandbox makes the attested decrypt call itself, then decrypts the file in guest memory.
One more page in this section is a reference rather than a blueprint:
- signer-policy: how a generated Cedar policy pins the container to an exact image reference, and how naming an AWS Signer signing profile instead relaxes that one pin. The three blueprints above link to it where they show their own image pin.
All three planned blueprints for this section are now here. See the roadmap.
Before you run one¶
Each blueprint's page lists what it needs, and they all share the
prerequisites on the home page: a cluster with the
confidential-sandbox runtime class, and the policy generator on your PATH. The Amazon
EKS User Guide, Cedar policies, names the generator.