AWS KMS Policy for TEE Auto-Unseal
The TEE auto-unseal path (PCR-bound(KMS-Shamir(master_key))) relies on AWS KMS enforcing kms:RecipientAttestation on decrypt to provide confidentiality. Without a correctly configured KMS resource policy, an IAM role with KMS access can decrypt the master key without a valid Nitro Enclave attestation document. This page provides a production-correct, lockout-safe KMS policy template.
Security model
Section titled “Security model”The outer PCR-AES-GCM layer provides integrity (PCR-drift detection), not confidentiality — PCR values are public. Confidentiality is provided entirely by the inner KMS-Shamir layer. Each KMS key must independently enforce kms:RecipientAttestation for decrypt in its resource policy.
Policy template (five-Statement, lockout-safe)
Section titled “Policy template (five-Statement, lockout-safe)”{ "Version": "2012-10-17", "Statement": [ { "Sid": "EnableIAMRootPermissionsBreakGlass", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::<account>:root" }, "Action": "kms:*", "Resource": "*" }, { "Sid": "AllowKeyAdministrators", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::<account>:role/ChamberKmsKeyAdmin" }, "Action": [ "kms:Create*", "kms:Describe*", "kms:Enable*", "kms:List*", "kms:Put*", "kms:Update*", "kms:Revoke*", "kms:Disable*", "kms:Get*", "kms:Delete*", "kms:TagResource", "kms:UntagResource", "kms:ScheduleKeyDeletion", "kms:CancelKeyDeletion" ], "Resource": "*" }, { "Sid": "AllowChamberEnclaveDecryptWithAttestation", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::<account>:role/ChamberEnclaveRole-<cluster-id>" }, "Action": "kms:Decrypt", "Resource": "*", "Condition": { "StringEqualsIgnoreCase": { "kms:RecipientAttestation:ImageSha384": [ "<current release ImageSha384 hex>", "<previous release ImageSha384 hex (during rollover only)>" ] } } }, { "Sid": "AllowChamberEnclaveEncryptForWrapping", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::<account>:role/ChamberEnclaveRole-<cluster-id>" }, "Action": "kms:Encrypt", "Resource": "*" }, { "Sid": "DenyDataOpsWithoutAttestation", "Effect": "Deny", "Principal": "*", "Action": [ "kms:Decrypt", "kms:GenerateDataKey", "kms:GenerateDataKeyPair", "kms:GenerateDataKeyPairWithoutPlaintext", "kms:GenerateDataKeyWithoutPlaintext", "kms:ReEncryptFrom", "kms:ReEncryptTo" ], "Resource": "*", "Condition": { "Null": { "kms:RecipientAttestation:ImageSha384": "true" } } } ]}Why each Statement exists
Section titled “Why each Statement exists”- EnableIAMRootPermissionsBreakGlass — Without this, destroying the admin role makes the key permanently unrecoverable (AWS lists this as the most common irrecoverable KMS error). Statement 5’s Deny still overrides root for decrypt and key-generation operations.
- AllowKeyAdministrators — Lifecycle management (rotate, policy update, disable, scheduled deletion). No
kms:Decrypt/kms:Encrypt/kms:GenerateDataKey*/kms:ReEncrypt*— admins must not be able to decrypt or wrap master-key shares. - AllowChamberEnclaveDecryptWithAttestation — Only the enclave role can decrypt, and only with a matching attestation document.
StringEqualsIgnoreCasewith a list-valued policy side matches if the request’s scalarImageSha384equals any element (supports two-version rolling-upgrade window). - AllowChamberEnclaveEncryptForWrapping — Allows the enclave to wrap newly generated or rotated Shamir shares. AWS KMS does not support
RecipientAttestationonEncrypt; the plaintext share originates inside the enclave and is sent to KMS over TLS. - DenyDataOpsWithoutAttestation — Explicit Deny on decrypt and key-generation actions when no attestation is present. The deny list intentionally excludes
kms:Encryptbecause encrypt requests cannot carryRecipientAttestation.
Condition key syntax
Section titled “Condition key syntax”Use StringEqualsIgnoreCase (not ForAnyValue:StringEqualsIgnoreCase). The kms:RecipientAttestation:ImageSha384 request-context key is a scalar string. ForAnyValue is for multi-valued request keys and is wrong here.
Each PCR is its own scalar condition key (kms:RecipientAttestation:PCR0, PCR1, PCR2, PCR8). There is no map-valued kms:RecipientAttestation:PCRs syntax.
Optional multi-PCR variant (defense-in-depth):
"Condition": { "StringEqualsIgnoreCase": { "kms:RecipientAttestation:ImageSha384": ["<curr>", "<prev>"], "kms:RecipientAttestation:PCR2": "<application binary hex>", "kms:RecipientAttestation:PCR8": "<signing cert hex>" }}Per-cluster IAM role requirement
Section titled “Per-cluster IAM role requirement”If two clusters share an enclave IAM role, cluster B’s enclave can decrypt cluster A’s TEE_AUTO_UNSEAL_BLOB blob (same image → same PCR0/ImageSha384 → attestation policy passes). The per-cluster suffix ChamberEnclaveRole-<cluster-id> blocks this.
Recommended cluster-id convention: <env>-<region>-<purpose> (e.g. prod-us-east-1-mainnet). Two clusters sharing a cluster-id defeat this mitigation silently.
Rolling-upgrade playbook (ImageSha384 rotation)
Section titled “Rolling-upgrade playbook (ImageSha384 rotation)”- Build new release; get new ImageSha384 from
nitro-cli describe-eif. - Update policy Statement 3 to include both old and new ImageSha384 hex values.
- Roll pods/instances to new image.
- Verify auto-unseal succeeds (
chamber_tee_unseal_total{status="success"}) andkms_attestation_rejectedis not incrementing. - Remove old hash from policy.
Anti-replay mitigation
Section titled “Anti-replay mitigation”After KMS key rotation: run kms:DisableKey on the retired key immediately (before or simultaneously with kms:ScheduleKeyDeletion). A disabled key refuses Decrypt requests. AWS does not support immediate deletion (minimum 7-day pending period). Propagation is typically seconds but may take minutes in degraded conditions — treat as not-yet-effective until a Decrypt against the retired ARN returns KMSInvalidStateException.
What must not change
Section titled “What must not change”Terraform snippet
Section titled “Terraform snippet”data "aws_iam_policy_document" "chamber_kms" { statement { sid = "EnableIAMRootPermissionsBreakGlass" effect = "Allow" principals { type = "AWS" identifiers = ["arn:aws:iam::${var.account_id}:root"] } actions = ["kms:*"] resources = ["*"] }
statement { sid = "AllowKeyAdministrators" effect = "Allow" principals { type = "AWS" identifiers = ["arn:aws:iam::${var.account_id}:role/ChamberKmsKeyAdmin"] } actions = [ "kms:Create*", "kms:Describe*", "kms:Enable*", "kms:List*", "kms:Put*", "kms:Update*", "kms:Revoke*", "kms:Disable*", "kms:Get*", "kms:Delete*", "kms:TagResource", "kms:UntagResource", "kms:ScheduleKeyDeletion", "kms:CancelKeyDeletion", ] resources = ["*"] }
statement { sid = "AllowChamberEnclaveDecryptWithAttestation" effect = "Allow" principals { type = "AWS" identifiers = ["arn:aws:iam::${var.account_id}:role/ChamberEnclaveRole-${var.cluster_id}"] } actions = ["kms:Decrypt"] resources = ["*"] condition { test = "StringEqualsIgnoreCase" variable = "kms:RecipientAttestation:ImageSha384" values = var.allowed_image_sha384s } }
statement { sid = "AllowChamberEnclaveEncryptForWrapping" effect = "Allow" principals { type = "AWS" identifiers = ["arn:aws:iam::${var.account_id}:role/ChamberEnclaveRole-${var.cluster_id}"] } actions = ["kms:Encrypt"] resources = ["*"] }
statement { sid = "DenyDataOpsWithoutAttestation" effect = "Deny" principals { type = "AWS" identifiers = ["*"] } actions = [ "kms:Decrypt", "kms:GenerateDataKey", "kms:GenerateDataKeyPair", "kms:GenerateDataKeyPairWithoutPlaintext", "kms:GenerateDataKeyWithoutPlaintext", "kms:ReEncryptFrom", "kms:ReEncryptTo", ] resources = ["*"] condition { test = "Null" variable = "kms:RecipientAttestation:ImageSha384" values = ["true"] } }}
resource "aws_kms_key" "chamber" { description = "Chamber TEE auto-unseal key for ${var.cluster_id}" deletion_window_in_days = 7 policy = data.aws_iam_policy_document.chamber_kms.json}