Most security controls on AWS decide who may call an API. Nitro Enclaves answer a different question: which code may see a secret once it is decrypted. An enclave is a small, hardened virtual machine carved out of an EC2 instance. It has its own kernel and its own share of vCPUs and memory, and it has no network interface, no persistent storage and no way to log in. Root on the parent instance cannot read its memory. The only way in or out is a local socket to the parent, and the only way the outside world can trust it is a signed attestation document that describes exactly which image booted.
This article explains the isolation model, building and measuring an enclave image, how attestation and AWS KMS fit together, a card-tokenisation example, and the failure modes and release process that keep you from locking yourself out of your own data.
What an enclave is, and what it is not
The Nitro Hypervisor already isolates EC2 instances from each other. Nitro Enclaves reuse that machinery inside a single instance: when you start an enclave, the hypervisor takes vCPUs and memory away from the parent instance and gives them to a new VM that the parent can no longer address. The enclave runs Linux, boots from an enclave image file (EIF), and talks to the parent over a virtual socket (vsock), an address family identified by a context ID and a port rather than an IP address. The parent is any supported Nitro-based instance running Linux or Windows Server 2016 or later; the enclave itself must run Linux.
Documented limits: up to four enclaves per parent, no enclave-to-enclave communication, enclaves die when the parent stops, no hibernation on enclave-enabled instances, and no extra charge beyond the instance.
Be precise about the threat model. An enclave protects data in use against everything on the parent, including root. It does not protect availability, since the parent decides whether requests are forwarded at all. It does not protect against bugs in your enclave code: a program that returns plaintext on request is a decryption service for whoever can reach it. And the hypervisor and attestation PKI remain the root of trust, so you still trust AWS.
Build: from container image to measurements
You package enclave code as a container image, then convert it with nitro-cli build-enclave. The output is an EIF plus a set of SHA-384 measurements. These measurements, held in platform configuration registers (PCRs), are the enclave's identity. Change one byte of the application and its measurement changes, which is the point and also the main operational hazard.
| PCR | Measures | When you know it | Typical use in policy |
|---|---|---|---|
| PCR0 | The enclave image file | Printed by build-enclave | Pin an exact build |
| PCR1 | Linux kernel and bootstrap | Printed by build-enclave | Pin the kernel and boot ramfs |
| PCR2 | Application, in order | Printed by build-enclave | Pin the user-space code |
| PCR3 | IAM role of the parent | SHA-384 of the role ARN | Only instances with this role |
| PCR4 | Parent instance ID | After launch | One specific instance; brittle |
| PCR8 | EIF signing certificate | Printed when you sign | Any image signed by your key |
PCR3 and PCR4 are computed by you: the guide shows a SHA-384 over 48 zero bytes followed by the role ARN or instance ID string. PCR8 appears only if you sign the EIF with --private-key and --signing-certificate. AWS recommends combining PCR3 and PCR8 for flexibility, because a policy pinned to PCR0 must change on every release, while a policy pinned to a signing certificate and a parent role survives rebuilds and instance replacement.
Reproducibility matters more than usual. If two builds of the same commit produce different PCR0 values, auditors cannot confirm that the measured image came from the reviewed source. Pin base images by digest rather than tag, pin package versions, avoid embedding build timestamps, and build twice in CI to compare the measurements.
# Build and record measurements in CI
docker build -t tokenizer-enclave:1.4.0 enclave/
nitro-cli build-enclave \
--docker-uri tokenizer-enclave:1.4.0 \
--output-file tokenizer-1.4.0.eif \
--private-key signing-key.pem \
--signing-certificate signing-cert.pem > measurements.json
# Run on the parent; debug mode is for development only (see below)
nitro-cli run-enclave --eif-path tokenizer-1.4.0.eif \
--cpu-count 2 --memory 2048 --enclave-cid 16
nitro-cli describe-enclaves
Running enclaves on the parent
The allocator service on the parent reserves memory and CPUs for enclaves from a small configuration file; if the reservation is smaller than what run-enclave asks for, the launch fails. The supported-type list excludes the smallest size in most families and many bare-metal sizes, so check it for yours. Drive nitro-cli from a systemd unit or orchestrator so the enclave restarts after a crash; nothing else will.
Starting an enclave with --debug-mode or --attach-console gives you a console but makes every PCR in the attestation document all zeroes. A policy pinning real PCRs refuses such an enclave, as it should; a policy accepting zeroes accepts anything. Keep debug enclaves on separate keys.
The attestation document
Inside the enclave, the Nitro Secure Module (NSM) device returns a signed attestation document on request. It is a CBOR-encoded structure wrapped in a COSE_Sign1 envelope signed with ECDSA over P-384. Its payload contains:
module_id,timestamp(milliseconds since the epoch) anddigest(always SHA384);pcrs, a map from index to value for the locked PCRs;certificateandcabundle, the signing certificate and the chain up to the AWS Nitro root;- three optional fields of up to 1,024 bytes each:
public_key,user_dataandnonce.
The optional fields are what turn a static statement into a protocol. A verifier sends a fresh nonce and checks it comes back, which proves the document is not a replay. The enclave puts a public key it generated in memory into public_key, so the verifier can encrypt a secret that only this enclave instance can open. And user_data can bind anything else, such as a hash of the TLS key the enclave will use.
KMS verifies these documents for you. If you build your own verifier, for example in a third-party key manager, the steps from the AWS guide are: decode the CBOR and the COSE_Sign1 structure, extract the document, build the chain from the target certificate through the CA bundle (which arrives root-first, so reverse it for tools that expect leaf-first), check every certificate is within its validity period, validate the chain against the AWS Nitro Enclaves root certificate with revocation checking disabled, and verify the signature. Pin the root by the fingerprint AWS publishes rather than trusting whatever root the bundle contains.
def verify_attestation(blob, expected, nonce, now):
# Pseudocode; use a maintained COSE/CBOR library, not hand-rolled parsing.
cose = cbor_decode(blob) # COSE_Sign1: [protected, unprotected, payload, sig]
doc = cbor_decode(cose.payload)
chain = [doc["certificate"]] + list(reversed(doc["cabundle"]))[:-1]
validate_chain(chain, trust_anchor=PINNED_NITRO_ROOT, at=now, revocation=False)
verify_es384(cose, public_key_of(doc["certificate"]))
if doc.get("nonce") != nonce:
raise Reject("stale or replayed document")
for index, value in expected.items(): # e.g. {3: role_pcr, 8: signer_pcr}
if doc["pcrs"][index].hex() != value:
raise Reject(f"PCR{index} mismatch")
if all(b == 0 for b in doc["pcrs"][0]):
raise Reject("debug-mode enclave")
return doc.get("public_key") # encrypt the secret to this key
KMS: making a key usable only by measured code
AWS KMS has built-in support for attestation. Five operations accept a signed attestation document in the request: Decrypt, DeriveSharedSecret, GenerateDataKey, GenerateDataKeyPair and GenerateRandom. When a document is present, KMS validates it and, instead of returning plaintext, encrypts the result under the public key from the document. Only the private key held in enclave memory can open it, so the parent instance that relayed the request sees ciphertext in both directions.
That is why the IAM credentials can live on the parent. The enclave has no instance metadata endpoint, so the parent passes it temporary credentials from the instance profile over vsock. Those credentials say who is calling; the attestation says which code receives the answer. The key policy ties them together with the kms:RecipientAttestation:ImageSha384 and kms:RecipientAttestation:PCR<n> condition keys. A request without an attestation document cannot satisfy this condition, but that protects nothing if another statement, IAM policy or grant also allows Decrypt. The default key policy gives the account full access and delegates to IAM, so replace it, for example using NotAction to exclude kms:Decrypt from the administrative statement, and audit IAM policies and grants.
{
"Sid": "DecryptOnlyInsideSignedTokenizerEnclave",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::123456789012:role/tokenizer-host"},
"Action": ["kms:Decrypt", "kms:GenerateDataKey"],
"Resource": "*",
"Condition": {
"StringEqualsIgnoreCase": {
"kms:RecipientAttestation:PCR3": "<sha384 of the parent role ARN>",
"kms:RecipientAttestation:PCR8": "<PCR8 printed by the signed build>"
}
}
}Producers need a separate statement to encrypt without attestation. Combine this with envelope encryption so KMS protects small data keys and the enclave processes bulk records locally; IAM policy conditions covers the condition mechanics.
The enclave cannot reach KMS itself, so a proxy on the parent forwards vsock connections to the regional endpoint. TLS terminates inside the enclave, so the proxy relays encrypted bytes; restrict it to an allowlist of endpoints. The Nitro Enclaves SDK provides helpers that attach the attestation document and unwrap the response, which is safer than assembling requests yourself.
Worked example: tokenising card numbers
A payments team wants API servers to return tokens for card numbers without any host holding the tokenisation key in plaintext. The flow:
- The API server on the parent receives a card number over TLS and writes it to the enclave over vsock. The parent sees the card number, so the enclave protects the key and vault, not the inbound value; protecting that needs clients to encrypt to an attested enclave key.
- At start-up the enclave generated a key pair in memory, obtained an attestation document containing the public key, and called KMS Decrypt on the encrypted tokenisation key with it. KMS checked PCR3 and PCR8 and returned the key encrypted to the enclave's public key.
- For each card number, the enclave computes a keyed token, stores the mapping in an encrypted vault record, and returns only the token. The parent writes the record to its database without being able to read it.
- Detokenisation is a separate, narrower operation only an internal settlement service can request.
A compromised API server can still misuse the interface, for example by requesting detokenisation if it can reach it. The enclave turns key theft into interface misuse, which is easier to rate-limit and audit; most of the real design work is in that interface.
Failure modes
| Symptom | Cause | Fix |
|---|---|---|
| KMS AccessDenied after a release | PCR0 or PCR2 changed and the policy pins it | Pin PCR8 plus PCR3, or add the new value before deploying |
| Works on one instance only | Policy pins PCR4, the instance ID | Drop PCR4 for fleets behind Auto Scaling |
| run-enclave fails with E36, E39 and E11 | The EIF signing certificate has expired | Track expiry, re-sign and update PCR8 in advance |
| Attestation always rejected in dev | Debug mode zeroes PCRs | Use separate dev keys with a dev policy |
| Enclave will not start | Allocator reserved less memory or CPU than requested | Raise the reservation and restart the allocator |
| KMS calls hang | Proxy down, or endpoint missing from its allowlist | Health-check the proxy; alarm on enclave KMS errors |
| Parent decrypts without attestation | Default key policy, IAM or a grant allows kms:Decrypt | Narrow the root statement; audit IAM and grants |
| Secrets leak anyway | Enclave API returns plaintext or an oracle | Return results, not keys; rate-limit sensitive operations |
The first row is the classic outage: a rebuilt image matches no pinned PCR0, and recovery is an emergency key-policy edit under pressure.
Operating enclaves over time
Treat measurements as release artifacts published by CI next to the image. If you pin PCR0, deploy in two steps: add the new value to the condition (a list of values matches any), roll out, then remove the old one. If you pin PCR8, rotate the signing certificate well before expiry and guard its private key like a KMS administrator credential, because whoever holds it can produce images your policy trusts.
KMS records attested requests in CloudTrail, showing which measurements call which keys. Enclave logs leave through vsock and must never contain secrets. Size EC2 parents knowing the enclave's memory and vCPUs are gone from the host.
Keep the trade-off honest. If you only need keys never to leave a hardware boundary, KMS already does that. Enclaves earn their complexity when your own code must process sensitive plaintext on hosts you do not fully trust, at the cost of debuggability and a stricter release process.
What to do next
- Write down the threat model in one paragraph: which secret, which attacker on the host, and which interface the enclave exposes.
- Run the Hello Enclaves sample on a supported instance type, then rebuild it twice and confirm PCR0 is reproducible.
- Sign your EIF, record PCR8, compute PCR3 from the parent role, and write a key policy that requires both.
- Prove the negative: call Decrypt from the parent with the same role and confirm AccessDenied, then from a debug enclave and confirm the same.
- Add the release procedure for measurements and the signing-certificate expiry to your runbook, with an alarm.
- Review the enclave's API as if the parent were hostile, and add rate limits and audit records to every operation that returns sensitive output.