Skip to content

Use ExternalId in Envelope Encryption by using KMS key from another account

0

We are trying to use cross account KMS key sharing. We can successfully use the KMS key to do Envelope encryption from another account by setting up the key policy. But, after we added the condition check with an ExternalId, it didn't work anymore. We want to get the help to illustrate how to use the ExternalId on the client side to do the Envelope encryption.

In the key owner side (666777888999), the key policy is

{
    "Version": "2012-10-17",
    "Id": "key-consolepolicy-3",
    "Statement": [
        {
            "Sid": "Enable IAM User Permissions",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::666777888999:root"
            },
            "Action": "kms:*",
            "Resource": "*"
        },
        {
            "Sid": "AllowAccessWithExternalId",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::111222333555:root"
            },
            "Action": [
                "kms:Encrypt",
                "kms:Decrypt",
                "kms:ReEncrypt*",
                "kms:GenerateDataKey*",
                "kms:DescribeKey"
            ],
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "sts:ExternalId": "aaaabbbb-a54d-4e7d-93ea-123456789012"
                }
            }
        }
    ]
}

On the client side in another account (111222333555), we use Node.JS.

We use KmsKeyringNode() to setup the key.

import {
    KmsKeyringNode,
    buildClient,
    CommitmentPolicy
} from '@aws-crypto/client-node';

import { env } from "../env.js";

const { encrypt, decrypt } = buildClient(
    CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT
)

function initializeKeyRing() {
    const generatorKeyId = env().GeneratorKeyId;
    const keyIds = [env().GeneratorKeyId];
    const keyRing = new KmsKeyringNode({
        generatorKeyId: generatorKeyId,
        keyIds: keyIds
    });
    return keyRing;
}

let keyRing;

const encryptData = async (plainText, context) => {
    if (typeof keyRing === 'undefined') {
        keyRing = initializeKeyRing();
    }

    try {
        const { result } = await encrypt(keyRing, plainText, { encryptionContext: context })
        return {
            encData: result,
            error: false
        }
    } catch (e) {
        console.log(e)
        return {
            error: true,
            msg: e
        }
    }
}

const dataContext = {
  "ExternalId": "aaaabbbb-a54d-4e7d-93ea-123456789012",
}

// Encrypt user data using KMS
async function encryptUserData(text) {
  console.log("Encrypting user data: ", text)

  const result = await kms.encryptData(text, dataContext);
  if (result.encData) {
      result.encData = result.encData.toString('base64');
  }
  return result;
}

If we remove the "Condition" clause in the key policy, it will work immediately. But, if we add the "Condition" clause back, the client side will fail and output this error:

AccessDeniedException: User: arn:aws:sts::111222333555:assumed-role/aaa/bbb is not authorized to perform: kms:GenerateDataKey on this resource because the resource does not exist in this Region, no resource-based policies allow access, or a resource-based policy explicitly denies access
... <code stack dump here> ...
  code: 'AccessDeniedException',
  '[__type]': 'See error.__type for details.',
  '[Message]': 'See error.Message for details.',
  time: 2024-12-28T02:23:59.543Z,
  requestId: '6e4ecb15-235d-4493-9926-48e3d481ccde',
  statusCode: 400,
  retryable: false,
  retryDelay: 14.504512522250867
}
asked a year ago212 views
2 Answers
1
Accepted Answer

At a quick glance, it looks like your client-side code is supplying ExternalId in the encryption context, which is a way to provide AAD (additional authenticated data) to cryptographic operations for use in making authorisation decisions.

Your key policy should use this type of comparison operation to evaluate the value provided in the encryption context (https://docs.aws.amazon.com/kms/latest/developerguide/conditions-kms.html#conditions-kms-encryption-context):

"Condition": {
	"StringEquals": {
		"kms:EncryptionContext:ExternalId": "aaaabbbb-a54d-4e7d-93ea-123456789012"
	}
}

You should also split the key policy statement granting the remote account access into two separate statements. One should contain the cryptographic operations that accept encryption context attributes, and the other statement should contain kms:DescribeKey which does not accept them and would always fail to match the condition.

The service authorisation reference shows which permissions accept encryption context attributes (and all other service-specific condition keys): https://docs.aws.amazon.com/service-authorization/latest/reference/list_awskeymanagementservice.html

EXPERT
answered a year ago
EXPERT
reviewed a year ago
  • Yes, after making this change to my key policy, it works! Thanks a lot!

1

The issue you're experiencing is related to how the ExternalId condition is being used in the key policy and how it's being passed in the client-side code. The ExternalId is typically used with IAM roles for cross-account access, but in your case, you're trying to use it directly with KMS operations.

To make this work, you need to modify your client-side code to pass the ExternalId when assuming a role that has permission to use the KMS key. Here's how you can adjust your approach:

  1. In the account that's using the KMS key (111222333555), create an IAM role with permissions to use the KMS key.

  2. In the key owner's account (666777888999), modify the key policy to allow the IAM role from the other account to use the key, instead of allowing the root account.

  3. In your client-side code, use the AWS SDK to assume the IAM role, passing the ExternalId when doing so.

  4. Use the temporary credentials obtained from assuming the role to create the KMS client.

Here's an example of how you might modify your code:

import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts";
import { KMSClient } from "@aws-sdk/client-kms";
import { KmsKeyringNode, buildClient, CommitmentPolicy } from '@aws-crypto/client-node';

const stsClient = new STSClient({ region: "your-region" });

async function assumeRole() {
  const params = {
    RoleArn: "arn:aws:iam::666777888999:role/CrossAccountKMSRole",
    RoleSessionName: "CrossAccountKMSSession",
    ExternalId: "aaaabbbb-a54d-4e7d-93ea-123456789012"
  };

  const command = new AssumeRoleCommand(params);
  const response = await stsClient.send(command);

  return response.Credentials;
}

async function initializeKeyRing() {
  const credentials = await assumeRole();
  
  const kmsClient = new KMSClient({
    credentials: {
      accessKeyId: credentials.AccessKeyId,
      secretAccessKey: credentials.SecretAccessKey,
      sessionToken: credentials.SessionToken
    },
    region: "your-region"
  });

  const generatorKeyId = env().GeneratorKeyId;
  const keyIds = [env().GeneratorKeyId];
  
  const keyRing = new KmsKeyringNode({
    generatorKeyId: generatorKeyId,
    keyIds: keyIds,
    clientProvider: () => kmsClient
  });

  return keyRing;
}

// Rest of your code remains the same

This approach ensures that the ExternalId is used when assuming the role, which then has the necessary permissions to use the KMS key. The KMS operations will be performed using the temporary credentials obtained from assuming the role, satisfying the ExternalId condition in the key policy.
Sources
Allowing users in other accounts to use a KMS key - AWS Key Management Service
Encrypt - AWS Key Management Service
AWS KMS condition keys - AWS Key Management Service

answered a year ago
EXPERT
reviewed a year ago
  • This answer seems making sense. But, I got this error

    TypeError: client.generateDataKey is not a function
        at Object.generateDataKey (/Users/slin/Work/aurascape/ai-mgmt/mgmt.lib/node_modules/@aws-crypto/kms-keyring/build/main/helpers.js:15:35)
        at KmsKeyringNode._onEncrypt (/Users/slin/Work/aurascape/ai-mgmt/mgmt.lib/node_modules/@aws-crypto/kms-keyring/build/main/kms_keyring.js:57:49)
        at KmsKeyringNode.onEncrypt (/Users/slin/Work/aurascape/ai-mgmt/mgmt.lib/node_modules/@aws-crypto/material-management/build/main/keyring.js:26:38)
        at NodeDefaultCryptographicMaterialsManager.getEncryptionMaterials (/Users/slin/Work/aurascape/ai-mgmt/mgmt.lib/node_modules/@aws-crypto/material-management-node/build/main/node_cryptographic_materials_manager.js:34:45)
        at Object._encryptStream (/Users/slin/Work/aurascape/ai-mgmt/mgmt.lib/node_modules/@aws-crypto/encrypt-node/build/main/encrypt_stream.js:46:10)
        at Object._encrypt (/Users/slin/Work/aurascape/ai-mgmt/mgmt.lib/node_modules/@aws-crypto/encrypt-node/build/main/encrypt.js:18:37)
    

You are not logged in. Log in to post an answer.

A good answer clearly answers the question and provides constructive feedback and encourages professional growth in the question asker.