Attest a Nitro Sandbox
A Nitro Sandbox in confidential mode can prove to an external service which code it runs and on which parent instance it runs. The Nitro System records measurements in the platform configuration registers (PCRs) of the Nitro Security Module (NSM) in the Nitro Sandbox. Your workload requests an attestation document from the NSM, and presents it to a service such as AWS Key Management Service (AWS KMS).
Attestation works like attestation for Nitro Enclaves. For more information, see Cryptographic attestation in the AWS Nitro Enclaves User Guide.
The measurements and the key policy rules apply to every Nitro Sandbox in confidential mode. The procedures use a Nitro Sandbox that you launch with Firecracker, as described in Run Nitro Sandboxes with Firecracker. For attestation of pods and tasks that Amazon Elastic Kubernetes Service (Amazon EKS) or Amazon Elastic Container Service (Amazon ECS) runs in Nitro Sandboxes, see Sandboxing on Amazon EKS with isolated and confidential pods in the Amazon EKS User Guide and Isolated and confidential tasks in the Amazon Elastic Container Service Developer Guide.
The Nitro System doesn't measure the contents of block devices. Put the code that you want to attest in the initial RAM disk, or have the initial RAM disk verify or decrypt the file systems that it mounts.
Contents
Nitro Sandbox measurements
A PCR starts as 48 zero bytes, and each measurement extends it: the new value is the SHA-384 hash of the old value followed by the measurement. The Nitro System locks PCRs 0 to 15 before the guest kernel runs.
The following table lists the PCRs of a Nitro Sandbox in confidential mode. All values are SHA-384 hashes.
| PCR | Measurement | Difference from Nitro Enclaves |
|---|---|---|
| PCR0 | The image: the kernel, the kernel command line, and the initial RAM disks, followed by the feature flags of the image. The flags record launch settings, such as CPU overcommit and hot plug, so a change to these settings changes PCR0. | Includes the feature flags. The value differs from the PCR0 that the
Nitro Enclaves CLI (nitro-cli) reports for the same
files. |
| PCR1 | The kernel, the kernel command line, and the first initial RAM disk. | None |
| PCR2 | The initial RAM disks after the first. If the image has one initial RAM disk, PCR2 holds the measurement of empty input. | None |
| PCR3 | The ARN of the IAM role in the instance profile of the parent instance, if the parent instance has one. | Keeps its value in debug mode. In Nitro Enclaves, it reads as zero in debug mode. |
| PCR4 | The instance ID of the parent instance. | Keeps its value in debug mode. In Nitro Enclaves, it reads as zero in debug mode. |
| PCR5 | The PCI functions that the Nitro Sandbox has at boot, or a fixed value when hot plug is on. | New for Nitro Sandboxes |
| PCR6 | The whole image file that the Nitro System boots. | New for Nitro Sandboxes |
| PCR16 to PCR31 | Not set by the Nitro System. Your workload can extend and lock them, for example to record a policy that it loads at runtime. | None |
After the VMM attaches the boot devices, and before the guest starts, the Nitro System extends PCR5 once for each PCI function, in bus, device, and function order. Each measurement is a 12-byte identity: the vendor ID, device ID, subsystem vendor ID, subsystem ID, base class and subclass, and a reserved zero field. The Nitro System doesn't measure PCI bridges or the NSM.
If the Nitro System measures no function, PCR5 holds the measurement of the
string EMPTY. If you enable hot plug, PCR5 holds the measurement of the
string HOTPLUG_ENABLED instead, because devices can change after boot, and
PCR5 does not identify the devices.
For an image that you build with eif_build, PCR6 is the value that
eif_build prints. The image records its build time, so PCR6 changes with
every build, unless you pass the same --build-time value to every build.
When Firecracker builds the image at launch, PCR6 also changes with the
memory size and the number of vCPUs. Use PCR0 instead of PCR6 in key policies.
In debug mode, PCR0, PCR1, PCR2, PCR5, and PCR6 read as zero, but PCR3 and PCR4 keep the values of the parent instance. A key policy that checks only PCR3 or PCR4 therefore also releases secrets to a Nitro Sandbox in debug mode. For how to write a key policy that prevents this, see Use attestation with AWS KMS.
Add attestation to a guest image
The following procedures extend the guest image from Prepare a guest image. At boot, the init script generates an RSA key pair, requests an attestation document that contains the public key, and sends the document to the parent instance over vsock. It then waits for the response from AWS KMS, decrypts the secret, and starts your workload.
The first procedure builds a small program, nsm-attest, that requests the attestation document from the NSM and writes it to a file. The program uses the NSM API library, as described in Get an attestation document.
To build the nsm-attest program
-
Install a C linker and the Rust toolchain, with the target for static binaries. For other ways to install Rust, see Install Rust
on the Rust website. sudo dnf install -y gcc curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal . "$HOME/.cargo/env" rustup target add $(uname -m)-unknown-linux-musl -
Create a directory for the program, and save the following content as
nsm-attest/Cargo.toml.mkdir -p nsm-attest/src[package] name = "nsm-attest" version = "0.1.0" edition = "2021" [dependencies] aws-nitro-enclaves-nsm-api = "0.4" serde_bytes = "0.11" -
Save the following content as
nsm-attest/src/main.rs.// Requests an attestation document from the NSM and writes it to a file. // Usage: nsm-attest attest --public-key key.der --output attestation.bin use aws_nitro_enclaves_nsm_api::api::{Request, Response}; use aws_nitro_enclaves_nsm_api::driver::{nsm_exit, nsm_init, nsm_process_request}; use serde_bytes::ByteBuf; use std::{env, fs, process::exit}; fn usage() -> ! { eprintln!("usage: nsm-attest attest --public-key FILE --output FILE"); exit(2); } fn main() { let args: Vec<String> = env::args().collect(); if args.get(1).map(String::as_str) != Some("attest") { usage(); } let (mut public_key, mut output) = (None, None); let mut i = 2; while i + 1 < args.len() { match args[i].as_str() { "--public-key" => public_key = Some(fs::read(&args[i + 1]).expect("read public key")), "--output" => output = Some(args[i + 1].clone()), _ => usage(), } i += 2; } let (Some(public_key), Some(output)) = (public_key, output) else { usage() }; let fd = nsm_init(); if fd < 0 { eprintln!("cannot open /dev/nsm"); exit(1); } let request = Request::Attestation { user_data: None, nonce: None, public_key: Some(ByteBuf::from(public_key)), }; let response = nsm_process_request(fd, request); nsm_exit(fd); match response { Response::Attestation { document } => fs::write(&output, document).expect("write document"), other => { eprintln!("NSM error: {:?}", other); exit(1); } } } -
Build the program as a static binary, so that it runs from the initial RAM disk, and install it in
/usr/local/bin.cd nsm-attest cargo build --release --target $(uname -m)-unknown-linux-musl sudo install -m 755 target/$(uname -m)-unknown-linux-musl/release/nsm-attest /usr/local/bin/ cd ..
To add attestation to the guest image
-
Install socat. The guest image and the parent instance use it to exchange files over vsock.
sudo dnf install -y socat -
In
sandbox-init, add the following lines before the line that starts your workload.# Request an attestation document that carries a new public key. cd /tmp openssl genrsa -out key.pem 2048 openssl rsa -in key.pem -pubout -outform DER -out key.der nsm-attest attest --public-key key.der --output attestation.bin # Send the document to the parent instance over vsock port 5005, and # receive the KMS response on port 5006. until socat -u OPEN:attestation.bin VSOCK-CONNECT:2:5005; do sleep 1; done until socat -u VSOCK-CONNECT:2:5006 CREATE:response.cms; do sleep 1; done openssl cms -decrypt -inform DER -in response.cms -inkey key.pem -out secret.binThe script waits until the parent instance completes the exchange in Use attestation with AWS KMS. Your workload can then read the secret from
/tmp/secret.bin. -
Build the initial RAM disk again, with the additional programs.
sudo dracut --force --no-hostonly --reproducible --no-early-microcode \ --modules bash \ --add-drivers "virtio_pci virtio_net vmw_vsock_virtio_transport nsm" \ --install "mount modprobe ip sleep openssl socat /usr/local/bin/nsm-attest /etc/pki/tls/openssl.cnf" \ --include "$PWD/sandbox-init" /init \ --kver $(uname -r) initrd.img sudo chown "$(id -un):" initrd.img
Compute the expected PCR values
You need the expected PCR values to write a key policy. To get them, build the image
in advance with eif_build, which prints the measurements of the image,
and launch that image. eif_build is part of the aws-nitro-enclaves-image-format
Build the image with the same settings that you launch the Nitro Sandbox with, such as CPU overcommit and hot plug. If the settings of the image don't match the launch configuration, the launch fails. The example builds an image for dedicated vCPUs, so the Nitro Sandbox doesn't use CPU overcommit.
To compute PCR0, PCR1, and PCR2
-
Install
eif_buildfrom the aws-nitro-enclaves-image-format project. The command uses the Rust toolchain from Add attestation to a guest image and installseif_buildin~/.cargo/bin.sudo dnf install -y openssl-devel cargo install --git https://github.com/aws/aws-nitro-enclaves-image-format eif_build -
Build the image from the files of Add attestation to a guest image, with the default command line of Firecracker.
eif_build --kernel vmlinuz \ --cmdline "panic=1 initcall_blacklist=i8042_init" \ --ramdisk initrd.img \ --pcie-virtio \ --output sandbox.eifThe
--pcie-virtiooption builds the image for a Nitro Sandbox in confidential mode without CPU overcommit or hot plug. -
Copy the PCR0, PCR1, and PCR2 values from the output. PCR0 depends on the files, the command line, and the settings. PCR1 and PCR2 depend only on the files and the command line.
The following is example output.
Output file: sandbox.eif { "HashAlgorithm": "Sha512_48 { .. }", "PCR0": "294af39e266a15be97de5d99b210f81fb9147044d9eb456ad808eeb8b2c2498cfebd4cf57e669228a2a8cde356722f25", "PCR1": "61f2b585b0994827830d1dbc2f0011cc30cdc9fd65e1e24eb99666442020676a1b9adf63008df638ca0a228e342d953c", "PCR2": "21b9efbc184807662e966d34f390821309eeac6802309798826296bf3e8bec7c10edb30948c90ba67310f7b964fc500a", "PCR6": "fb9718a5397717ed57cb42c2a6c4dea98e22f4bc21df1808ad8dde4ed8c1c450c123fdab9f2fa3c065e005357a768be6" }
-
In
sandbox.jsonfrom Launch a Nitro Sandbox, setkernel_image_pathto the image file, removeinitrd_path, and removecpu_overcommitfrom theenclaveobject. On instances with two threads per core, use an even number of vCPUs."boot-source": { "kernel_image_path": "sandbox.eif" }
PCR3 and PCR4 extend the zero value with the role ARN and the instance ID as text. Compute them with the following commands. If you change the instance profile of the parent instance, PCR3 changes.
python3 -c 'import hashlib, sys; print(hashlib.sha384(b"\0" * 48 + sys.argv[1].encode()).hexdigest())'arn:aws:iam::111122223333:role/parent-instance-rolepython3 -c 'import hashlib, sys; print(hashlib.sha384(b"\0" * 48 + sys.argv[1].encode()).hexdigest())'i-1234567890abcdef0
PCR5 depends on the devices that the parent instance attaches and on the order in
which Firecracker adds them. To get the expected value, launch the Nitro
Sandbox once with the final device configuration, and read PCR5 in the Nitro Sandbox
with the DescribePCR request of the NSM API or from an attestation
document. Use PCR5 in a key policy only if your device configuration does not
change.
Get an attestation document
In a Nitro Sandbox in confidential mode, the Linux nsm driver
(CONFIG_NSM) exposes the NSM as /dev/nsm. If the
driver is a module, as in the AL2023 kernel, load it with modprobe nsm
before you request a document.
To request an attestation document, use the NSM API library from the aws-nitro-enclaves-nsm-apiaws-nitro-enclaves-nsm-api Rust crate and a C library
(libnsm). Pass the public key of an RSA key pair that your workload
generates, because AWS KMS encrypts its response to this key. Build your program as a
static binary, or include its libraries in the initial RAM disk.
You can also pass a nonce and user data for your own verifier. The same library extends and locks PCRs 16 to 31.
To use attestation with your own service, verify the signature and certificate chain
of the attestation document. The chain ends at the same AWS Nitro Enclaves root
certificate as for Nitro Enclaves. For more information, see the NSM attestation process
Use attestation with AWS KMS
AWS KMS accepts attestation documents from a Nitro Sandbox for the
Decrypt, DeriveSharedSecret, GenerateDataKey,
GenerateDataKeyPair, and GenerateRandom operations. When a
request includes an attestation document, AWS KMS does not return plaintext. It returns
CiphertextForRecipient, which only the private key in the Nitro Sandbox
can decrypt. For more information, see Condition keys for
Nitro Enclaves in the AWS Key Management Service Developer Guide.
The following procedure uses an AWS KMS key. If you need a new key, use the create-key command, which prints the ARN of the key.
aws kms create-key --description "Nitro Sandbox secrets" \ --query KeyMetadata.Arn --output text
To release a secret only to an attested Nitro Sandbox
-
Save the key policy of your AWS KMS key as
policy.json, and add the following statement to itsStatementarray.aws kms get-key-policy \ --key-idarn:aws:kms:us-east-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab\ --policy-name default \ --query Policy \ --output text > policy.json{ "Sid": "AllowDecryptFromAttestedNitroSandbox", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::111122223333:role/parent-instance-role" }, "Action": [ "kms:Decrypt", "kms:GenerateDataKey" ], "Resource": "*", "Condition": { "StringEqualsIgnoreCase": { "kms:RecipientAttestation:PCR0": "PCR0_value", "kms:RecipientAttestation:PCR3": "PCR3_value" } } }The statement allows the role of the parent instance to decrypt and to create data keys only for a Nitro Sandbox that runs the expected image on a parent instance with the expected IAM role. Replace the PCR values with the values from Compute the expected PCR values. Use the
kms:RecipientAttestation:PCRcondition keys with theNStringEqualsIgnoreCaseoperator. Thekms:RecipientAttestation:ImageSha384condition key compares PCR0. In a key policy,"Resource": "*"refers to the AWS KMS key that the policy is attached to, not to all keys.Always include PCR0, PCR1, or
kms:RecipientAttestation:ImageSha384in the condition. PCR3 and PCR4 identify only the parent instance, which also chooses the image that it launches, and they keep their values in debug mode. Make sure that no other policy allows the principal to use the key without the attestation condition. -
Apply the key policy with the put-key-policy command. The command replaces the whole policy, so keep the statements that give your key administrators access.
aws kms put-key-policy \ --key-idarn:aws:kms:us-east-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab\ --policy-name default \ --policy file://policy.json -
Encrypt the secret with the AWS KMS key. Use a principal that has
kms:Encryptpermission, for example on your workstation, and use the encrypt command. Then copysecret.encto the parent instance.aws kms encrypt \ --key-idarn:aws:kms:us-east-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab\ --plaintext fileb://secret.txt\ --query CiphertextBlob \ --output text | base64 --decode > secret.enc -
Launch the image that you built in Compute the expected PCR values, with the
sandbox.jsonthat you changed there. If the Nitro Sandbox from Launch a Nitro Sandbox still runs, stop it first, so that it releases the TAP device, and remove its socket file. Run the command in the directory that containssandbox.eif.sudo rm -f/run/sandbox/v.socksudo firecracker --no-api --config-file sandbox.jsonRun the commands in the following steps in another terminal on the parent instance.
-
Receive the attestation document. When the Nitro Sandbox connects to port
Nof CID 2, Firecracker forwards the connection to the Unix socketon the parent instance. The following command returns after the Nitro Sandbox has sent the document. The attestation document is not secret.uds_path_Nsudo socat -u UNIX-LISTEN:/run/sandbox/v.sock_5005 CREATE:attestation.bin -
Use the decrypt command with the attestation document. The parent instance uses its own credentials.
aws kms decrypt \ --key-idarn:aws:kms:us-east-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab\ --ciphertext-blob fileb://secret.enc \ --recipient KeyEncryptionAlgorithm=RSAES_OAEP_SHA_256,AttestationDocument@=fileb://attestation.bin \ --query CiphertextForRecipient \ --output text | base64 --decode > response.cmsTo create a data key instead, use the generate-data-key command. The following example keeps both parts of the response. Store
data-key.enc, so that you can decrypt the data key again later with the decrypt command.aws kms generate-data-key \ --key-idarn:aws:kms:us-east-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab\ --key-spec AES_256 \ --recipient KeyEncryptionAlgorithm=RSAES_OAEP_SHA_256,AttestationDocument@=fileb://attestation.bin \ --query '[CiphertextBlob,CiphertextForRecipient]' \ --output text > data-key.txt cut -f1 data-key.txt | base64 --decode > data-key.enc cut -f2 data-key.txt | base64 --decode > response.cms -
Send the response to the Nitro Sandbox on port 5006. The init script decrypts it with its private key into
secret.bin, and then starts your workload. The parent instance can't decrypt the response, because AWS KMS encrypts it to the public key in the attestation document.sudo socat -u OPEN:response.cms UNIX-LISTEN:/run/sandbox/v.sock_5006
If the attestation document does not match the key policy, AWS KMS returns an
AccessDeniedException error.
The Nitro Sandbox can also call AWS KMS itself with the same command, if its guest
image contains the AWS Command Line Interface (AWS CLI). For this, the parent instance gives it a network
path to the AWS KMS endpoint, such as a network interface with NAT, credentials for the
key policy principal, and the Region, for example in the AWS_REGION
environment variable. The credentials don't need to be secret from the parent instance,
because the attestation condition determines whether AWS KMS releases the secret.