How to Build an Enclave Image File for AWS Nitro Enclaves?

Building and packaging a Docker container image into an Enclave Image File for Amazon EC2 Nitro Enclaves requires preparing a compatible container image and converting it using the AWS Nitro Enclaves Command Line Interface. This guide outlines the hardware prerequisites, installation steps, container configuration requirements, and the build commands needed to generate a signed or unsigned Enclave Image File along with its cryptographic measurements for attestation.

Prerequisites and Instance Configuration

To build and run an Enclave Image File (EIF), you need an Amazon EC2 instance that supports AWS Nitro Enclaves, or a compatible Linux build environment with Docker installed. While you can build an EIF on any x86_64 or ARM64 Linux machine with Docker and the Nitro CLI installed, running the enclave requires a parent EC2 instance launched with the Nitro Enclaves option enabled.

Before starting the build process, ensure your environment meets the following criteria:

  • An Amazon Linux 2, Amazon Linux 2023, or Ubuntu Linux host matching the target CPU architecture (x86_64 or aarch64).
  • Docker Engine installed, running, and accessible by your current user account.
  • Sufficient disk space to hold both the uncompressed Docker image layers and the resulting .eif file (the final EIF size is roughly equal to the uncompressed container image size plus kernel and initramfs overhead, \(S_{\text{EIF}} \approx S_{\text{image}} + S_{\text{kernel}} + S_{\text{initramfs}}\)).

Installing the AWS Nitro Enclaves CLI and Docker

On Amazon Linux 2023, you can install the AWS Nitro Enclaves CLI and Docker directly from the standard system repositories:

sudo dnf install -y aws-nitro-enclaves-cli aws-nitro-enclaves-cli-devel docker

On Amazon Linux 2, use the Amazon Extras library to install the required packages:

sudo amazon-linux-extras install aws-nitro-enclaves-cli -y
sudo yum install -y aws-nitro-enclaves-cli-devel docker

After installing the packages, add your user to the ne and docker security groups so you can execute build commands without elevated root privileges, and start the Docker service:

sudo usermod -aG ne $USER
sudo usermod -aG docker $USER
sudo systemctl enable --now docker

Log out and log back in for the group membership changes to take effect. Verify the installation by checking the version of the CLI tool:

nitro-cli --version

Creating a Compatible Docker Container Image

An enclave has no persistent storage, no external network interface (no TCP/IP stack connected to the internet or VPC), and no interactive shell access. Instead, it communicates exclusively with its parent EC2 instance over a local virtual socket (vsock) connection.

When authoring a Dockerfile for a Nitro Enclave, keep the following constraints in mind:

  1. Entrypoint or CMD is required: The nitro-cli build-enclave command extracts the ENTRYPOINT or CMD directive from the Docker image metadata and uses it as the initial process started inside the enclave.
  2. Self-contained dependencies: All libraries, binaries, and static assets must be baked directly into the container image because the enclave cannot download packages at runtime.
  3. Read-only root filesystem considerations: The enclave root filesystem runs entirely in memory (initramfs). Ensure your application writes temporary data to /tmp or /run and respects the memory allocation assigned to the enclave.

Below is an example of a Python application (app.py) that listens on a vsock port inside the enclave, followed by its corresponding Dockerfile:

import socket

VSOCK_PORT = 5000
VMADDR_CID_ANY = 0xFFFFFFFF

def main():
    s = socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM)
    s.bind((VMADDR_CID_ANY, VSOCK_PORT))
    s.listen()
    print(f"Listening on vsock port {VSOCK_PORT}...")
    while True:
        conn, addr = s.accept()
        data = conn.recv(1024)
        if data:
            conn.sendall(b"Ack: " + data)
        conn.close()

if __name__ == "__main__":
    main()

Create a Dockerfile in the same directory:

FROM public.ecr.aws/amazonlinux/amazonlinux:2023

RUN dnf install -y python3 && dnf clean all

WORKDIR /app
COPY app.py /app/app.py

CMD ["python3", "/app/app.py"]

Build the local Docker image using the standard Docker build command:

docker build -t vsock-sample:latest .

Packaging the Docker Image into an Enclave Image File

Once the Docker image is present in your local Docker daemon or accessible in a remote container registry, use the nitro-cli build-enclave command to package it into an Enclave Image File (.eif).

Run the following command to convert the vsock-sample:latest Docker image into an EIF named vsock-sample.eif:

nitro-cli build-enclave \
  --docker-uri vsock-sample:latest \
  --output-file vsock-sample.eif

If you have a Dockerfile and want nitro-cli to build the Docker image and convert it in a single step, you can also pass the --docker-dir flag pointing to the build context directory alongside --docker-uri:

nitro-cli build-enclave \
  --docker-dir ./ \
  --docker-uri vsock-sample:latest \
  --output-file vsock-sample.eif

During the build process, nitro-cli pulls or inspects the container layers, flattens the filesystem into an initial RAM filesystem (initramfs), bundles the Nitro Enclaves Linux kernel and bootstrap init process, and writes the final binary image to vsock-sample.eif.

Understanding Build Output and PCR Measurements

When nitro-cli build-enclave finishes, it outputs a JSON object to standard output containing cryptographic Platform Configuration Register (PCR) measurements calculated using SHA-384:

{
  "Measurements": {
    "HashAlgorithm": "Sha384 { ... }",
    "PCR0": "a1b2c3d4e5f6...",
    "PCR1": "112233445566...",
    "PCR2": "998877665544..."
  }
}

Each register serves a specific purpose in cryptographic attestation and AWS Key Management Service (KMS) access policies:

  • PCR0 (Enclave Image): A cryptographic hash of the entire Enclave Image File, including the kernel, bootstrap binaries, and application filesystem. Any modification to the application code or base image changes PCR0.
  • PCR1 (Linux Kernel and Bootstrap): A hash of the kernel and boot ramfs provided by AWS Nitro Enclaves.
  • PCR2 (Application): A hash of the user space application root filesystem extracted from the Docker container image, excluding the kernel and bootstrap components.

You can inspect an existing .eif file at any time without rebuilding it by running nitro-cli describe-eif:

nitro-cli describe-eif --eif-path vsock-sample.eif

Signing an Enclave Image File for PCR8 Attestation

In production workflows, updating an application changes PCR0 and PCR2, which requires updating IAM or AWS KMS key policies that rely on those registers. To authorize enclaves by their build signer rather than a specific image hash, you can sign the EIF with an elliptic curve (ECDSA P-384) private key and an X.509 certificate during the build phase. Signing populates PCR8 with the hash of the signing certificate.

First, generate an ECDSA P-384 private key and a self-signed certificate using OpenSSL:

openssl ecparam -name secp384r1 -genkey -out signing-key.pem
openssl req -new -x509 -key signing-key.pem -sha384 -days 365 -out signing-cert.pem -subj "/CN=EnclaveSigner"

Next, run nitro-cli build-enclave with the --private-key and --signing-certificate flags:

nitro-cli build-enclave \
  --docker-uri vsock-sample:latest \
  --output-file vsock-sample-signed.eif \
  --private-key signing-key.pem \
  --signing-certificate signing-cert.pem

The resulting JSON output will now include a PCR8 value alongside PCR0, PCR1, and PCR2. You can reference PCR8 in AWS KMS key policy conditions (kms:RecipientAttestation:PCR8) so any enclave image signed by your key and certificate is authorized to decrypt sensitive secrets.

Verifying and Running the Enclave Image File

To test the packaged .eif file on a Nitro Enclave-enabled EC2 instance, verify that the Nitro Enclaves allocator service is configured with sufficient memory and CPU cores in /etc/nitro_enclaves/allocator.yaml, restart the nitro-enclaves-allocator.service, and launch the enclave:

nitro-cli run-enclave \
  --cpu-count 2 \
  --memory 1024 \
  --eif-path vsock-sample.eif \
  --debug-mode

Note that --debug-mode allows you to view standard output from the container via nitro-cli console --enclave-id <ENCLAVE_ID>, which is helpful during initial development and testing. However, when --debug-mode is enabled, the Nitro Hypervisor zeros out all PCR values in the attestation document (\(PCR_i = 0\)), preventing production KMS attestation checks from succeeding until the flag is removed.