Do not use when the workload does not handle sensitive data that requires hardware-level isolation, when the instance type does not support Nitro Enclaves (requires Nitro-based instances with at least 4 vCPUs), or when latency constraints make the vsock communication overhead unacceptable.
nitro-cli toolset installed on the parent EC2 instance (Amazon Linux 2 or AL2023)aws-nitro-enclaves-sdk-c or Python aws-encryption-sdk for enclave-side KMS operations/etc/nitro_enclaves/allocator.yaml
Set up the parent EC2 instance to support enclave launches:
sudo amazon-linux-extras install aws-nitro-enclaves-cli
sudo yum install aws-nitro-enclaves-cli-devel -y
sudo systemctl enable --now nitro-enclaves-allocator.service
sudo systemctl enable --now docker
sudo usermod -aG ne ec2-user
sudo usermod -aG docker ec2-user
/etc/nitro_enclaves/allocator.yaml to reserve resources for the enclave. The enclave requires dedicated memory that is carved from the parent instance:
---
memory_mib: 4096
cpu_count: 2
Restart the allocator: sudo systemctl restart nitro-enclaves-allocator.service
nitro-cli describe-enclaves to confirm the CLI can communicate with the Nitro hypervisor. An empty JSON array [] indicates no enclaves are running and the setup is correct.Package the sensitive workload into a signed enclave image:
Create the application Dockerfile: The enclave runs a minimal Linux environment. The application communicates exclusively through vsock:
FROM amazonlinux:2
RUN yum install -y python3 python3-pip && \
pip3 install boto3 cbor2 cryptography requests
COPY enclave_app.py /app/enclave_app.py
WORKDIR /app
CMD ["python3", "enclave_app.py"]
Build the EIF with nitro-cli: Convert the Docker image into an enclave image file, capturing the PCR measurements:
docker build -t enclave-app:latest .
nitro-cli build-enclave \
--docker-uri enclave-app:latest \
--output-file enclave-app.eif
The output contains three critical PCR values:
Build a signed EIF (recommended for production): Generate a signing certificate and use it to produce PCR8:
openssl ecparam -name secp384r1 -genkey -noout -out enclave_key.pem
openssl req -new -key enclave_key.pem -sha384 \
-nodes -subj "/CN=Enclave Signer" -out enclave_csr.pem
openssl x509 -req -days 365 -in enclave_csr.pem \
-signkey enclave_key.pem -sha384 -out enclave_cert.pem
nitro-cli build-enclave \
--docker-uri enclave-app:latest \
--output-file enclave-app.eif \
--private-key enclave_key.pem \
--signing-certificate enclave_cert.pem
PCR8 (the signing certificate hash) enables KMS policies that trust any image signed by a specific certificate, allowing image updates without changing the policy.
Create a KMS key policy that restricts decryption to a verified enclave:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowEnclaveDecrypt",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:role/EnclaveParentRole"
},
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "*",
"Condition": {
"StringEqualsIgnoreCase": {
"kms:RecipientAttestation:ImageSha384": "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
}
}
}
]
}
{
"Condition": {
"StringEqualsIgnoreCase": {
"kms:RecipientAttestation:PCR8": "ab3456789012345678901234567890123456789012345678901234567890123456789012345678901234567890abcdef"
}
}
}
{
"Condition": {
"StringEqualsIgnoreCase": {
"kms:RecipientAttestation:PCR0": "<pcr0-hex>",
"kms:RecipientAttestation:PCR1": "<pcr1-hex>"
}
}
}
kms:Decrypt permission, but the KMS key policy condition ensures the actual decryption only succeeds when the request originates from a valid enclave with the correct attestation document attached.Establish the parent-to-enclave communication channel:
3, and the enclave CID is assigned at launch.import socket
import json
import boto3
VSOCK_CID = 3 # Parent CID
VSOCK_PORT = 5000
def start_proxy():
sock = socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM)
sock.bind((VSOCK_CID, VSOCK_PORT))
sock.listen(5)
kms_client = boto3.client('kms', region_name='us-east-1')
while True:
conn, addr = sock.accept()
data = conn.recv(65536)
request = json.loads(data.decode())
if request['action'] == 'decrypt':
response = kms_client.decrypt(
CiphertextBlob=bytes.fromhex(request['ciphertext']),
Recipient={
'KeyEncryptionAlgorithm': 'RSAES_OAEP_SHA_256',
'AttestationDocument': bytes.fromhex(request['attestation_doc'])
}
)
conn.sendall(json.dumps({
'ciphertext_for_recipient': response['CiphertextForRecipient'].hex()
}).encode())
conn.close()
/dev/nsm, attaches it to KMS decrypt requests, and receives data encrypted to the enclave's ephemeral public key:
import socket
import json
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization
PARENT_CID = 3
VSOCK_PORT = 5000
def get_attestation_document(public_key_der):
"""Request attestation document from NSM device."""
# Uses the aws-nitro-enclaves-nsm-api
# NSM provides: module_id, digest (SHA384), timestamp, PCRs,
# certificate (from Nitro PKI), cabundle, public_key, user_data, nonce
import nsm_util
nsm_fd = nsm_util.nsm_lib_init()
attestation_doc = nsm_util.nsm_get_attestation_doc(
nsm_fd,
public_key=public_key_der,
user_data=None,
nonce=None
)
return attestation_doc
def decrypt_via_parent(ciphertext_hex):
"""Send decrypt request through vsock to parent proxy."""
private_key = rsa.generate_private_key(
public_exponent=65537, key_size=2048
)
public_key_der = private_key.public_key().public_bytes(
serialization.Encoding.DER,
serialization.PublicFormat.SubjectPublicKeyInfo
)
attestation_doc = get_attestation_document(public_key_der)
sock = socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM)
sock.connect((PARENT_CID, VSOCK_PORT))
sock.sendall(json.dumps({
'action': 'decrypt',
'ciphertext': ciphertext_hex,
'attestation_doc': attestation_doc.hex()
}).encode())
response = json.loads(sock.recv(65536).decode())
sock.close()
# KMS encrypted the plaintext to the enclave's public key
# Only the enclave's private key can decrypt it
ciphertext_for_recipient = bytes.fromhex(
response['ciphertext_for_recipient']
)
plaintext = private_key.decrypt(
ciphertext_for_recipient,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
return plaintext
Verify attestation documents from enclaves to establish trust:
Attestation document structure: The document is CBOR-encoded and COSE-signed (COSE_Sign1). It contains:
module_id: Identifier for the NSM moduledigest: Hashing algorithm (SHA-384)timestamp: Unix epoch milliseconds when the document was createdpcrs: Map of PCR index to measurement value (PCR0-PCR15)certificate: The NSM's x509 certificate, signed by the Nitro PKIcabundle: Certificate chain from the NSM certificate to the AWS Nitro root CApublic_key: The enclave's ephemeral public key (provided at attestation request time)user_data: Optional application-defined data (up to 512 bytes)nonce: Optional nonce for freshness verificationValidation steps:
https://aws-nitro-enclaves.amazonaws.com/AWS_NitroEnclaves_Root-G1.zip)aws.nitro-enclaves CNAttestation validation code:
import cbor2
from cose import CoseMessage
from cryptography import x509
from cryptography.x509.oid import NameOID
def validate_attestation(attestation_bytes, expected_pcrs, expected_nonce=None):
cose_msg = CoseMessage.decode(attestation_bytes)
payload = cbor2.loads(cose_msg.payload)
# Verify certificate chain
cert = x509.load_der_x509_certificate(payload['certificate'])
cabundle = [x509.load_der_x509_certificate(c) for c in payload['cabundle']]
# Check root CA is AWS Nitro
root = cabundle[-1]
cn = root.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
assert cn == 'aws.nitro-enclaves', f'Unexpected root CA: {cn}'
# Verify PCR measurements
pcrs = payload['pcrs']
for idx, expected_value in expected_pcrs.items():
actual = pcrs.get(idx, b'').hex()
assert actual == expected_value, f'PCR{idx} mismatch: {actual}'
# Verify nonce freshness
if expected_nonce:
assert payload.get('nonce') == expected_nonce, 'Nonce mismatch'
return payload
Run the enclave and implement operational monitoring:
Launch the enclave:
nitro-cli run-enclave \
--eif-path enclave-app.eif \
--cpu-count 2 \
--memory 4096 \
--enclave-cid 16 \
--debug-mode
Note: --debug-mode enables the enclave console for development. Remove it in production as it allows reading enclave output, which breaks the isolation guarantee.
Verify enclave status:
nitro-cli describe-enclaves
Expected output includes "State": "RUNNING", the assigned EnclaveCID, memory, CPU count, and enclave flags.
Read enclave console (debug mode only):
nitro-cli console --enclave-id <enclave-id>
Terminate the enclave:
nitro-cli terminate-enclave --enclave-id <enclave-id>
CloudWatch monitoring: Configure the parent instance to report enclave health metrics. Since the enclave has no network access, health checks must go through the vsock proxy:
# Parent-side health check over vsock
def check_enclave_health(enclave_cid, port=5001):
try:
sock = socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM)
sock.settimeout(5)
sock.connect((enclave_cid, port))
sock.sendall(b'HEALTH_CHECK')
response = sock.recv(1024)
sock.close()
return response == b'OK'
except (socket.timeout, ConnectionRefusedError):
return False
| Term | Definition |
|---|---|
| Nitro Enclave | An isolated virtual machine created by the Nitro Hypervisor on a Nitro-based EC2 instance with no persistent storage, no network access, and no interactive access, even from the parent instance's root user |
| Attestation Document | A CBOR-encoded, COSE-signed document generated by the Nitro Security Module containing PCR measurements, a certificate chain to the AWS Nitro root CA, and optional user-provided data |
| PCR (Platform Configuration Register) | SHA-384 hash measurements that uniquely identify an enclave's image (PCR0), kernel/bootstrap (PCR1), application (PCR2), IAM role (PCR4), instance ID (PCR3), and signing certificate (PCR8) |
| Vsock | A virtual socket providing the sole communication channel between a parent EC2 instance and its enclave, using CID (Context Identifier) and port addressing |
| EIF (Enclave Image File) | The packaged enclave image built by nitro-cli from a Docker image, containing the kernel, ramdisk, and application, producing PCR measurements at build time |
| Nitro Security Module (NSM) | A custom Linux device (/dev/nsm) inside the enclave that provides attestation document generation and hardware random number generation |
| COSE_Sign1 | CBOR Object Signing and Encryption single-signer structure used to sign the attestation document with the NSM's private key |
| kms:RecipientAttestation | AWS KMS condition key prefix that enables key policies to enforce that decrypt/generate operations only succeed when a valid attestation document with matching PCR values is presented |
Decrypt and GenerateDataKey operations that include Recipient parameters, enabling auditing of enclave-originated cryptographic operationsContext: A healthcare SaaS company processes patient records containing PHI. Regulations require that the decryption and tokenization of PHI never occurs on an instance accessible to operators. The company deploys a Nitro Enclave that receives encrypted patient records, decrypts them inside the enclave using KMS with attestation, tokenizes the PII fields, and returns only the tokenized records through the vsock.
Approach:
kmstool-enclave-cli binary, and a vsock server that accepts encrypted recordsnitro-cli build-enclave and record PCR0, PCR1, PCR2 from the build outputkms:RecipientAttestation:ImageSha384 condition matching PCR0, allowing only this specific enclave build to decrypt patient recordskms:Decrypt on the key, but the KMS condition ensures decryption only succeeds inside the attested enclave{ssn: "tok_a8f3...", dob: "tok_b2e1...", name: "tok_c9d4..."}
Decrypt calls with RecipientAttestation parameters, confirming all decryption occurs within the enclave boundaryPitfalls:
allocator.yaml, causing the enclave to fail at launch with an opaque "resource not available" error## Nitro Enclave Security Assessment
**Enclave Image**: enclave-tokenizer.eif
**Build Date**: 2026-03-19T14:30:00Z
**Instance Type**: m5.2xlarge
**Allocated Resources**: 2 vCPUs, 4096 MiB memory
### PCR Measurements
| PCR | Value | Bound in KMS Policy |
|-----|-------|---------------------|
| PCR0 (Image) | a1b2c3d4e5f6... | Yes |
| PCR1 (Kernel) | f6e5d4c3b2a1... | Yes |
| PCR2 (Application) | 1a2b3c4d5e6f... | No |
| PCR8 (Signing Cert) | 9f8e7d6c5b4a... | Yes (production) |
### KMS Key Policy Verification
- Key ARN: arn:aws:kms:us-east-1:111122223333:key/mrk-abc123
- Attestation condition: kms:RecipientAttestation:ImageSha384 = PCR0
- Signing cert condition: kms:RecipientAttestation:PCR8 = <cert-hash>
- Parent role: arn:aws:iam::111122223333:role/EnclaveParentRole
- Direct decrypt from parent: BLOCKED (attestation required)
- Decrypt from verified enclave: ALLOWED
### Security Posture
- [PASS] Debug mode disabled in production launch command
- [PASS] Vsock is the only communication channel (no network interface)
- [PASS] Attestation document nonce verification implemented
- [PASS] Certificate chain validates to AWS Nitro root CA
- [WARN] PCR0 used in policy; consider PCR8 for deployment flexibility
- [FAIL] Health check endpoint does not verify enclave attestation freshness