Skip to content

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.

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"
}
}
}
]
}
  1. 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.
  2. 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.
  3. AllowChamberEnclaveDecryptWithAttestation — Only the enclave role can decrypt, and only with a matching attestation document. StringEqualsIgnoreCase with a list-valued policy side matches if the request’s scalar ImageSha384 equals any element (supports two-version rolling-upgrade window).
  4. AllowChamberEnclaveEncryptForWrapping — Allows the enclave to wrap newly generated or rotated Shamir shares. AWS KMS does not support RecipientAttestation on Encrypt; the plaintext share originates inside the enclave and is sent to KMS over TLS.
  5. DenyDataOpsWithoutAttestation — Explicit Deny on decrypt and key-generation actions when no attestation is present. The deny list intentionally excludes kms:Encrypt because encrypt requests cannot carry RecipientAttestation.

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>"
}
}

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)”
  1. Build new release; get new ImageSha384 from nitro-cli describe-eif.
  2. Update policy Statement 3 to include both old and new ImageSha384 hex values.
  3. Roll pods/instances to new image.
  4. Verify auto-unseal succeeds (chamber_tee_unseal_total{status="success"}) and kms_attestation_rejected is not incrementing.
  5. Remove old hash from policy.

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.

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
}