Sharing Encrypted AMI with KMS - Deep Dive.
Focus :
- Intro,
- Updating the KMS Key Policy (Source Account),
- Sharing the AMI (Source Account),
- Configuring Target Account Permissions,
- Launching the Instance (Target Account),
- Special Case with Auto Scaling Groups,
- Core Concept Recap (what AMI consists of),
- The Encryption Relationship,
- What Actually Happens When twtech Shares an Encrypted AMI,
- Step-1: twtech shares the encrypted AMI (CLI)
- Step-2: twtech Adds a key policy entry for the KMS key (Sample policy),
- Step-3: twtech Adds launch permissions on the AMI (CLI),
- Understanding KMS Key Sharing Implications,
- What Permissions Are Actually Needed (in target or destination account),
- Cross-Region AMI Copy with KMS,
- Cross-Account Copy (Best Practice),
- Common Pitfalls,
- Security Design Patterns,
- Observability and Auditing,
- Quick Checklist for Encrypted AMI Sharing,
- Key Takeaways,
- Insights.
Intro:
- Sharing an encrypted Amazon Machine Image (AMI) across AWS accounts requires sharing both the AMI itself and the specific Customer Managed Key (CMK) used for its encryption.
- twtech cannot share AMIs encrypted with AWS-managed keys (like
aws/ebs).
- Permissions needed: The target account (or specific IAM roles/users within it) requires:
kms:DescribeKey,kms:CreateGrant,kms:Decrypt,kms:ReEncrypt*.- Principal: Use the target account's root ARN (e.g.,
arn:aws:iam::DESTINATION_ACCOUNT:root) to allow the target account's administrators to delegate these permissions further.
- Once the key is shared, twtech must provide launch permissions for the AMI to the target account.
- Navigate to the EC2 Console,
- select the AMI,
- choose Modify Image Permissions,
- Enter the Account ID of the destination account and save.
- twtech IAM user or role in the target account that will launch instances from the shared AMI must have an IAM policy allowing it to use the source account's KMS key.
- Action Include:
-
kms:Decrypt kms:DescribeKeypointing to the source key's ARN.
- Find the AMI under Shared with me in the EC2 Launch Instance Wizard.
- During the launch, twtech can choose to re-encrypt the volumes with a KMS key owned by the target account to maintain independent control over the data.
- If twtech shares AMI with an Auto Scaling Group (ASG), it must create a KMS Grant for the ASG's Service-Linked Role.
- This is because the ASG service needs persistent permission to use the key to launch instances on your behalf.
Core idea:
- To securely share the AMI across accounts or regions.
1. Core Concept Recap (what AMI consists of):
- One or more EBS snapshots (backing volumes),
- Launch permissions (which accounts can use the AMI to launch instances),
- Block device mappings (metadata).
NB:
- When twtech
encrypts an AMI (typically using
aws ec2 create-imagefrom an encrypted EBS volume or with--kms-key-id), the underlying snapshots are encrypted with a KMS key. - Sharing an unencrypted AMI is straightforward
- But when snapshots are encrypted with KMS, there’s an additional layer:
- The target account must have permission to use the KMS key that encrypted the snapshots, otherwise they can’t decrypt or launch instances from that AMI.
2. The
Encryption Relationship
- Each EBS snapshot is encrypted with a single KMS key (CMK).
- The AMI’s encryption state is derived from those snapshots.
- The AMI metadata itself isn’t encrypted; encryption applies at the snapshot level.
sharing an encrypted AMI:
- The AMI launch permission must allow the target account.
- The EBS snapshot(s) must be shared with the same account(s).
- The KMS key
policy must allow those accounts to use the key
for decryption (specifically for
DescribeKey,ReEncrypt,CreateGrant,Encrypt,Decrypt,GenerateDataKey*).
3. What Actually Happens When twtech Shares an Encrypted AMI
- Source Account: 111111111111
- Target Account: 222222222222
- KMS key ARN:
arn:aws:kms:us-east-2:111111111111:key/twtecchkmskey-abcd-1234... - Snapshot ARN:
arn:aws:ec2:us-east-1:111111111111:snapshot/twtechsnap-0abc123...
Step-1: twtech shares the
encrypted AMI
# Share the
underlying snapshots
aws ec2 modify-snapshot-attribute \ --snapshot-id snap-0abc123... \ --attribute createVolumePermission \ --operation-type add \ --user-ids 222222222222Step-2: twtech Adds a key policy entry for the KMS key (Sample policy).
twtech must
update the key policy in account 111111111111 to allow account 222222222222 to
use the key.
# json{ "Sid": "twtech Allows use of the key for shared AMI", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::222222222222:root" }, "Action": [ "kms:Decrypt", "kms:DescribeKey", "kms:ReEncrypt*", "kms:CreateGrant", "kms:GenerateDataKey*" ], "Resource": "*"}NB:
- This allows the target account to create a volume from the snapshot (via EBS APIs) using that key.
Step-3: twtech Adds launch
permissions on the AMI (CLI)
# bashaws ec2 modify-image-attribute \ --image-id ami-0abcd1234... \ --launch-permission "Add=[{UserId=222222222222}]"NB:
- Now, the target account (destination) can see and launch from that AMI.
- Dut they’re still dependent on twtech KMS key.
4.
Understanding KMS Key Sharing Implications
- When twtech shares a KMS-encrypted resource (snapshot, AMI, etc.), twtech is granting cryptographic operations, not ownership.
- Decrypt the data (only for EBS use cases),
- Create a copy of the snapshot (and re-encrypt with their own key).
What the target account can NOT do:
- Directly manage or delete twtech key,
- Revoke or rotate twtech key,
- Use it outside of EBS for other operations.
NB:
- This distinction is critical for security and compliance.
5. What Permissions Are Actually Needed (target account):
kms:Decrypt— to decrypt snapshot data blocks during volume creation.kms:GenerateDataKey*— for creating data keys to encrypt/decrypt during volume use.kms:DescribeKey— for the EBS service to validate the key.kms:CreateGrant— sometimes used internally by EBS.
#Sample minimal safe
key policy :
# json{ "Sid": "twtech Allow EBS in target account to use key", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::222222222222:root" }, "Action": [ "kms:Decrypt", "kms:DescribeKey", "kms:ReEncrypt*", "kms:CreateGrant", "kms:GenerateDataKey*" ], "Resource": "*", "Condition": { "StringEquals": { "kms:ViaService": "ec2.us-east-2.amazonaws.com" } }}NB:
- This limits use to the EC2 service, not arbitrary KMS requests.
6.
Cross-Region AMI Copy with KMS
- twtech cannot directly share an encrypted AMI
across regions.
Instead: - Instead twtech:
1. Copies the AMI to
another region.
2. During the copy process, it specifies a destination KMS key
(in the
target region).
Sample:
aws ec2copy-image\--source-image-id ami-0abcd1234 \--source-region us-east-2 \--region us-west-1 \--name "twtechEncryptedCopy" \--kms-key-id arn:aws:kms:us-west-1:111111111111:key/twtechkmskey-abcd-5678
NB:
- twtech can re-encrypt during copy.
- This is the most common practice to change ownership or key control between regions.
7.
Cross-Account Copy (Best Practice)
When the target account wants its own control over
encryption:
1. twtech shares
the source snapshot and KMS key.
2. Target account copies the snapshot, specifying its
own KMS key.
Example
(in target account):
aws ec2 copy-snapshot \ --source-region us-east-2 \ --source-snapshot-id snap-0abc123 \ --kms-key-id arn:aws:kms:us-east-2:222222222222:key/twtechkmskey-efgh-9999NB:
- This creates a new, independently encrypted snapshot in the target account using their own key.
- Now they can register their own AMI from that snapshot.
- no dependency on twtech key anymore.
- Recommended for production handoffs or regulated environments.
8. Common
Pitfalls
|
Issue |
Root Cause |
Resolution |
|
Target account cannot see snapshot |
Snapshot not shared |
Run |
|
Target cannot launch AMI |
KMS key not shared |
Update KMS key policy |
|
Launch fails with “AccessDeniedException” |
|
Add KMS permission for target |
|
Target AMI creation fails |
Source snapshot in different region |
Copy snapshot to same region first |
|
Still can’t decrypt |
Forgot |
Recheck KMS key policy condition block |
|
Using multi-account org |
Service Control Policies (SCPs) blocking KMS use |
Adjust SCPs or grant exception |
9. Security
Design Patterns
Pattern A — Producer–Consumer
(Simple Sharing)
- One producer account creates golden AMIs.
- Several consumer accounts (dev/test/prod) can launch but not re-encrypt.
- KMS key is owned and controlled by producer.
- AMI and KMS key shared explicitly.
Pros:
Simpler setup, central governance.
Cons:
Consumers depend on producer’s KMS key; can’t rotate independently.
Pattern B — Producer–Consumer
(Decoupled Ownership)
- Producer shares AMI + KMS key temporarily.
- Consumer copies snapshot → re-encrypts with its own key.
- Producer then revokes KMS sharing.
Pros:
Full isolation, independent key management.
Cons:
One-time manual copy per version.
Pattern C — Automated
Pipeline
- CI/CD pipeline builds golden AMI (encrypted).
- After build, pipeline shares snapshot + key.
- Target accounts automatically copy & re-encrypt using AWS Lambda or EventBridge.
- Key access automatically revoked post-copy.
Pros: Secure, automated, scalable across many accounts.
Cons:
Requires automation maturity.
10.
Observability and Auditing
Monitor via:
·
CloudTrail:
o
CreateGrant,
Decrypt, GenerateDataKey events on your KMS key.
o
ModifyImageAttribute
and ModifySnapshotAttribute
events.
·
AWS Config:
o
Track cross-account resource sharing.
·
EventBridge
+ SNS:
o
Notify on AMI or snapshot sharing changes.
Security tip:
Add a KMS key usage alarm that alerts if unexpected principals
use the key outside normal AMI workflows.
11. Quick
Checklist for Encrypted AMI Sharing
|
Step |
Description |
|
Enable encryption on source AMI (SSE-KMS) |
Use your CMK |
|
Share underlying snapshot(s) |
Modify snapshot attributes |
|
Update KMS key policy |
Allow target accounts |
|
Add AMI launch permissions |
Modify image attributes |
|
Verify in target account |
DescribeImages, LaunchInstance test |
|
(Optional) Copy snapshot with target’s KMS key |
To decouple ownership |
|
Revoke access if needed |
Remove key policy and snapshot sharing |
12. Key
Takeaways
- AMI sharing ≠ snapshot sharing ≠ KMS sharing — you must do all three.
- The KMS key is the real gatekeeper — without it, no decryption or volume creation happens.
- For secure long-term sharing:
o Share only temporarily,
o Automate snapshot copy and re-encryption in target,
o Keep key policies scoped via
kms:ViaServiceandaws:PrincipalArnconditions.
twtech insights
Here’s a complete JSON bundle that ties together all the
moving parts required to share an encrypted AMI across AWS
accounts in a secure and compliant way.
This bundle
covers:
1. KMS key policy (source account)
2. Snapshot sharing (source account)
3. AMI launch permission modification (source account)
NB:
It is designed for cross-account sharing (one producer →
one consumer).
Scenario
Setup
|
Role |
Value |
Description |
|
|
Source account ID |
|
Owner of the encrypted AMI |
|
|
Target account ID |
|
Account that will use or copy the AMI |
|
|
Region |
|
Must be the same for both snapshot and AMI |
|
|
AMI ID |
|
The encrypted AMI you’re sharing |
|
|
Snapshot ID |
|
Snapshot backing the AMI |
|
|
KMS Key ID |
|
The CMK used for encryption |
|
1, KMS Key Policy (Source Account)
This key policy allows:
- Source account (owner) to administer the key,
- Target account (consumer) to use the key only for EC2 operations in the same region.
# json{ "Version": "2012-10-17", "Id": "key-policy-for-ami-share", "Statement": [ { "Sid": "twtech Allow key administration by key owner", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::111111111111:root" }, "Action": "kms:*", "Resource": "*" }, { "Sid": "twtech Allow use of key by EC2 and target account for AMI decryption", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::222222222222:root" }, "Action": [ "kms:Decrypt", "kms:DescribeKey", "kms:ReEncrypt*", "kms:CreateGrant", "kms:GenerateDataKey*" ], "Resource": "*", "Condition": { "StringEquals": { "kms:ViaService": "ec2.us-east-2.amazonaws.com" } } } ]}NB:
- The
kms:ViaServicecondition restricts the target to using this key only through the EC2 service (no arbitrary KMS calls). - twtech can further tighten this with
aws:SourceArn=arn:aws:ec2:us-east-2:111111111111:snapshot/snap-0abc123456789def0if twtech wants to allow decryption for that specific snapshot only.
2, Snapshot Sharing (Source Account)
Snapshots are shared at the EBS layer using the createVolumePermission
attribute.
# Here’s the
JSON version of that operation’s effect:
{ "Operation": "ModifySnapshotAttribute", "SnapshotSharing": { "TargetAccount": "222222222222", "Region": "us-east-2", "Action": "add", "Attribute": "createVolumePermission", "SnapshotId": "snap-0abc123456789def0" }}# Equivalent
AWS CLI command:
aws ec2 modify-snapshot-attribute \ --snapshot-id snap-0abc123456789def0 \ --attribute createVolumePermission \ --operation-type add \ --user-ids 222222222222Result:
The target account can now see and
copy this snapshot if they have KMS access to the encryption key.
3, AMI Launch Permission (Source Account)
Finally, share the AMI itself by granting the target account launch permissions. This affects the AMI’s metadata (not encryption or snapshots).
{ "Operation": "ModifyImageAttribute", "AmiSharing": { "TargetAccount": "222222222222", "Region": "us-east-1", "Action": "add", "Attribute": "launchPermission", "ImageId": "ami-0abcd1234ef567890" }}NB:
# Equivalent
AWS CLI command:
aws ec2 modify-image-attribute \ --image-id ami-0abcd1234ef567890 \ --launch-permission "Add=[{UserId=222222222222}]"Result:
Target account can now launch EC2 instances from this AMI —
as long as they can decrypt the snapshots (i.e.,
KMS permissions work).
4, Optional: Copying Snapshot with Target’s Own KMS
Key (Best Practice)
# To
decouple from the source’s key, the target account can re-encrypt the snapshot:
{ "Operation": "CopySnapshot", "SourceRegion": "us-east-2", "SourceSnapshotId": "snap-0abc123456789def0", "DestinationKmsKeyId": "arn:aws:kms:us-east-1:222222222222:key/twteechkmskey-5015-6016-7777-888888888888", "DestinationRegion": "us-east-1"}# Equivalent
AWS CLI command:
aws ec2 copy-snapshot \ --source-region us-east-2 \ --source-snapshot-id snap-0abc123456789def0 \ --kms-key-id arn:aws:kms:us-east-1:222222222222:key/twteechkmskey-5015-6016-7777-888888888888Result- The target account now owns a new, independently encrypted snapshot.
- They can create their own AMI from that snapshot.
- twtech can safely remove their access to your KMS key and snapshot.
5, Optional Tightened KMS Key Policy (if
twtech wants to scope even narrower)
# twtech can limit the target’s permissions to the
exact snapshot only, like this:
{ "Sid": "Allow target account to decrypt specific snapshot only", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::accountID:root" }, "Action": [ "kms:Decrypt", "kms:DescribeKey", "kms:GenerateDataKey*" ], "Resource": "*", "Condition": { "StringEquals": { "kms:ViaService": "ec2.us-east-2.amazonaws.com" }, "ArnLike": { "aws:SourceArn": "arn:aws:ec2:us-east-1:111111111111:snapshot/snap-0abc123456789def0" } }}NB:
This ensures the key can’t be used by the target for any other snapshots or
operations.
6, Verification
Flow
|
Step |
Command |
Expected
Outcome |
|
1 |
|
Snapshot visible in source |
|
2 |
|
Snapshot visible (shared) |
|
3 |
|
AMI visible |
|
4 |
In target account, |
Shared AMI visible |
|
5 |
Launch EC2 instance from AMI |
Succeeds if KMS access correct |
|
6 |
(Optional) Copy snapshot with target’s KMS key |
Creates independent snapshot |
Key
Takeaways
·
AMI sharing across accounts requires 3
separate permissions:
1, AMI launch permission
2, Snapshot sharing
3, KMS key sharing
- Always test with one snapshot first before sharing full multi-volume AMIs.
- The safest production pattern is: share → copy → re-encrypt → revoke.
- The kms:ViaService condition is your friend — it locks usage to EC2, reducing exposure.
- A Sample production-ready CloudFormation template that automates encrypted AMI creation and cross-account sharing with:
- Proper KMS key permissions,
- Snapshot sharing,
- AMI launch permissions.
- This version is clean, auditable, and deployable
in the source account (the
one that owns the encrypted AMI and KMS key).
- The target account ID is provided as a parameter so you can reuse this template easily for different consumers.
# CloudFormation Template for Encrypted AMI Sharing Stack
AWSTemplateFormatVersion: 2010-09-09Description: > Deploys a secure, repeatable setup for sharing a KMS-encrypted AMI (including snapshot and KMS key permissions) with a target AWS account.Parameters: TargetAccountId: Type: String Description: AWS Account ID of the target (consumer) account. AmiId: Type: String Description: The AMI ID to share (must already exist and be encrypted). SnapshotId: Type: String Description: The Snapshot ID backing the AMI. Region: Type: String Default: us-east-2 Description: AWS Region of the AMI and snapshot. KmsKeyAlias: Type: String Default: shared-ami-key Description: Alias name for the KMS CMK used for encryption.Resources: ################################################## # 1, Create or reference a KMS CMK for AMI encryption ################################################## AmiEncryptionKey: Type: AWS::KMS::Key Properties: Description: KMS CMK used to encrypt the AMI and snapshots for sharing EnableKeyRotation: true KeyPolicy: Version: "2012-10-17" Id: key-policy-for-ami-share Statement: # Admin permissions for the key owner (this account) - Sid: AllowKeyAdministration Effect: Allow Principal: AWS: !Sub arn:aws:iam::${AWS::AccountId}:root Action: "kms:*" Resource: "*" # Allow EC2 service + target account to use the key for AMI decryption - Sid: twtechAllowTargetAccountUse Effect: Allow Principal: AWS: !Sub arn:aws:iam::${TargetAccountId}:root Action: - kms:Decrypt - kms:DescribeKey - kms:ReEncrypt* - kms:CreateGrant - kms:GenerateDataKey* Resource: "*" Condition: StringEquals: kms:ViaService: !Sub ec2.${Region}.amazonaws.com AmiEncryptionKeyAlias: Type: AWS::KMS::Alias Properties: AliasName: !Sub alias/${KmsKeyAlias} TargetKeyId: !Ref AmiEncryptionKey ################################################## # 2, Snapshot sharing permission ################################################## SnapshotSharePermission: Type: AWS::EC2::Snapshot Properties: # CloudFormation doesn’t natively modify snapshot attributes, # so this section assumes the snapshot already exists and uses a custom resource. # The custom resource calls ModifySnapshotAttribute to share the snapshot. # In a real stack, you can embed a Lambda-backed custom resource for this action. # Placeholder metadata to document the intended permission: Tags: - Key: SharedWith Value: !Ref TargetAccountId - Key: Purpose Value: SharedEncryptedAMI ################################################## # 3, Custom Resource — Share Snapshot & AMI (Lambda) ################################################## ShareAutomationLambdaRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ShareEncryptedAmiRole-${AWS::StackName} AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: twtechSharePermissionsPolicy PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - ec2:ModifySnapshotAttribute - ec2:ModifyImageAttribute - ec2:DescribeImages - ec2:DescribeSnapshots Resource: "*" ShareAutomationLambda: Type: AWS::Lambda::Function Properties: Description: "Shares encrypted AMI and snapshot with target account" Handler: index.handler Runtime: python3.12 Timeout: 60 Role: !GetAtt ShareAutomationLambdaRole.Arn Code: ZipFile: | import boto3 import cfnresponse def handler(event, context): ec2 = boto3.client('ec2') try: target = event['ResourceProperties']['TargetAccountId'] ami = event['ResourceProperties']['AmiId'] snap = event['ResourceProperties']['SnapshotId'] # Share snapshot ec2.modify_snapshot_attribute( SnapshotId=snap, Attribute='createVolumePermission', OperationType='add', UserIds=[target] ) # Share AMI ec2.modify_image_attribute( ImageId=ami, LaunchPermission={'Add': [{'UserId': target}]} ) cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) except Exception as e: print("Error:", e) cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': str(e)}) ShareAutomationInvoke: Type: Custom::ShareAutomation Properties: ServiceToken: !GetAtt ShareAutomationLambda.Arn TargetAccountId: !Ref TargetAccountId AmiId: !Ref AmiId SnapshotId: !Ref SnapshotIdOutputs: SharedAMI: Description: The encrypted AMI that was shared with the target account. Value: !Ref AmiId SharedSnapshot: Description: The snapshot backing the AMI that was shared. Value: !Ref SnapshotId SharedKmsKey: Description: The ARN of the KMS key allowing cross-account AMI use. Value: !Ref AmiEncryptionKey How the
automation process Works
1. Creates a KMS CMK
with a scoped key policy
o
Grants decrypt and re-encrypt permissions to the
target account only through EC2.
2. Attaches an alias
for easy key management (alias/shared-ami-key).
3. Invokes a Lambda-backed custom resource that:
o
Shares the EBS snapshot (modify-snapshot-attribute)
o
Adds AMI launch permissions (modify-image-attribute)
4. Outputs the
key, snapshot, and AMI info for reference or automation chaining.
Deployment
Steps
1. Deploy the Stack in Source Account
aws cloudformation deploy \ --template-file twtechencrypted-ami-share.yaml \ --stack-name twtechEncryptedAmiShare \ --parameter-overrides \ TargetAccountId=222222222222 \ AmiId=ami-0abcd1234ef567890 \ SnapshotId=snap-0abc123456789def0 \ Region=us-east-22. In Target Account
# The target can now see and use
the AMI:
aws ec2 describe-images --executable-users self# Or make a copy of the snapshot
using their own KMS key:
aws ec2 copy-snapshot --source-region us-east-2 \--source-snapshot-id snap-0abc123456789def0 \--kms-key-id arn:aws:kms:us-west-1:222222222222:key/twtecehkmskey-efgh-5555-6666-7777-8888 Best
Practices
- Rotate the KMS key annually (enabled automatically).
- If the target copies and re-encrypts snapshots, revoke their key access afterward:
- Remove target principal from the KMS key policy.
- Optionally remove snapshot and AMI sharing attributes.
- Use stack outputs as inputs to CI/CD pipelines that automate AMI promotion across accounts.
- Here’s a smple advanced multi-account CloudFormation version of the encrypted AMI sharing stack.
- This version scales the previous template to share an encrypted AMI (and its snapshot + KMS key) with multiple target accounts or an entire AWS Organization.
- It’s clean, production-ready, and built for large environments that promote golden AMIs across Dev / QA / Prod or via AWS Organizations OU sharing.
CloudFormation
Template for Multi-Account Encrypted AMI Sharing
AWSTemplateFormatVersion: 2010-09-09Description: > Secure, repeatable setup for sharing a KMS-encrypted AMI and snapshot across multiple AWS accounts or AWS Organizations (Dev, QA, Prod, etc.)Parameters: TargetAccountIds: Type: CommaDelimitedList Description: > List of AWS Account IDs to share AMI and KMS key with (e.g. Dev, QA, Prod). Example: 111111111111,222222222222,333333333333 OrganizationId: Type: String Default: "" Description: > (Optional) AWS Organization ID (e.g. o-abcd1234). If set, sharing will include all accounts in the organization. AmiId: Type: String Description: Existing encrypted AMI ID to share. SnapshotId: Type: String Description: Snapshot ID backing the AMI. Region: Type: String Default: us-east-2 Description: AWS Region of the AMI and snapshot. KmsKeyAlias: Type: String Default: shared-ami-key Description: Alias name for the KMS CMK used for encryption.Resources: ################################################## # 1, KMS CMK for AMI/Snapshot Encryption ################################################## AmiEncryptionKey: Type: AWS::KMS::Key Properties: Description: twtech Multi-account CMK used for encrypted AMI sharing EnableKeyRotation: true KeyPolicy: Version: "2012-10-17" Id: multi-account-key-policy Statement: # Admin rights for this account - Sid: AllowKeyAdministration Effect: Allow Principal: AWS: !Sub arn:aws:iam::${AWS::AccountId}:root Action: "kms:*" Resource: "*" # Allow EC2 service + target accounts to use the key - Sid: twtechAllowTargetAccountsUse Effect: Allow Principal: AWS: !Ref TargetAccountIds Action: - kms:Decrypt - kms:DescribeKey - kms:ReEncrypt* - kms:CreateGrant - kms:GenerateDataKey* Resource: "*" Condition: StringEquals: kms:ViaService: !Sub ec2.${Region}.amazonaws.com # Optionally allow AWS Organization - Sid: twtechAllowOrganizationUse Effect: Allow Principal: "*" Action: - kms:Decrypt - kms:DescribeKey - kms:ReEncrypt* - kms:CreateGrant - kms:GenerateDataKey* Resource: "*" Condition: StringEqualsIfExists: aws:PrincipalOrgID: !Ref OrganizationId kms:ViaService: !Sub ec2.${Region}.amazonaws.com AmiEncryptionKeyAlias: Type: AWS::KMS::Alias Properties: AliasName: !Sub alias/${twtechKmsKey-Alias} TargetKeyId: !Ref AmiEncryptionKey ################################################## # 2, Lambda Role to Automate Sharing ################################################## ShareAutomationLambdaRole: Type: AWS::IAM::Role Properties: RoleName: !Sub MultiAccountAmiShareRole-${AWS::StackName} AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/twtechAWSLambdaBasicExecutionRole Policies: - PolicyName: twtechSharePermissionsPolicy PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - ec2:ModifySnapshotAttribute - ec2:ModifyImageAttribute - ec2:DescribeImages - ec2:DescribeSnapshots Resource: "*" ################################################## # 3, Lambda Function to Share AMI/Snapshot ################################################## ShareAutomationLambda: Type: AWS::Lambda::Function Properties: Description: "twtech Shares encrypted AMI and snapshot with multiple target accounts or org" Handler: index.handler Runtime: python3.12 Timeout: 120 Role: !GetAtt ShareAutomationLambdaRole.Arn Code: ZipFile: | import boto3 import cfnresponse def handler(event, context): ec2 = boto3.client('ec2') try: props = event['ResourceProperties'] targets = [a.strip() for a in props['TargetAccountIds'].split(',') if a.strip()] ami = props['AmiId'] snap = props['SnapshotId'] org_id = props.get('OrganizationId', '') if org_id: print(f"Sharing with Organization ID: {org_id}") ec2.modify_snapshot_attribute( SnapshotId=snap, Attribute='createVolumePermission', OperationType='add', UserGroups=['all'] ) ec2.modify_image_attribute( ImageId=ami, LaunchPermission={'Add': [{'Group': 'all'}]} ) # Share explicitly with target accounts for account in targets: print(f"Sharing with Account ID: {account}") ec2.modify_snapshot_attribute( SnapshotId=snap, Attribute='createVolumePermission', OperationType='add', UserIds=[account] ) ec2.modify_image_attribute( ImageId=ami, LaunchPermission={'Add': [{'UserId': account}]} ) cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Targets': targets}) except Exception as e: print("Error:", e) cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': str(e)}) ################################################## # 4, Custom Resource to Trigger Sharing ################################################## ShareAutomationInvoke: Type: Custom::MultiAccountAmiShare Properties: ServiceToken: !GetAtt ShareAutomationLambda.Arn TargetAccountIds: !Join [",", !Ref TargetAccountIds] OrganizationId: !Ref OrganizationId AmiId: !Ref AmiId SnapshotId: !Ref SnapshotIdOutputs: SharedAMI: Description: Encrypted AMI shared across multiple target accounts. Value: !Ref AmiId SharedSnapshot: Description: Snapshot backing the AMI. Value: !Ref SnapshotId SharedKmsKeyArn: Description: ARN of the multi-account KMS key. Value: !Ref AmiEncryptionKey SharedAccounts: Description: Target accounts that received sharing permissions. Value: !Join [", ", !Ref TargetAccountIds]How the deployment process Works (Multi-Account
/ Org Mode)
KMS Key
- Grants decrypt + re-encrypt rights to all target
accounts listed in
TargetAccountIds. - If
OrganizationIdis specified, it also enables org-wide use for that Org ID viaaws:PrincipalOrgID.
Lambda Automation
- Iterates through all target accounts and runs:
-
ModifySnapshotAttributeto grant EBS snapshot access. -
ModifyImageAttributeto add AMI launch permission. - If
OrganizationIdis set, adds an Org-wide sharing group.
Custom Resource
- Automatically runs the Lambda at deployment and re-runs on updates.
Sample
Deployments process
# Share to Three Accounts (Dev, QA, Prod)
aws cloudformation deploy \ --template-file twtech multi-account-ami-share.yaml \ --stack-name MultiAccountAmiShare \ --parameter-overrides \ TargetAccountIds=111111111111,222222222222,333333333333 \ AmiId=ami-0abcd1234ef567890 \ SnapshotId=snap-0abc123456789def0 \ Region=us-east-2# Share to Entire AWS Organization
aws cloudformation deploy \ --template-file twtechmulti-account-ami-share.yaml \ --stack-name OrgWideAmiShare \ --parameter-overrides \ OrganizationId=twtechOrgId-abcd1234 \ AmiId=ami-0abcd1234ef567890 \ SnapshotId=snap-0abc123456789def0 Security
& Ops Recommendations
- Rotate KMS
CMKs annually (enabled automatically here).
- Use
Organization sharing for central AMI distribution (less operational overhead).
- Revoke
permissions after promotion or retirement:
aws ec2 modify-image-attribute --image-id ami-xxxx --launch-permission "Remove=[{UserId=111111111111}]"- Integrate
with CodePipeline or Jenkins for AMI promotion workflows.
- Enable
CloudTrail for
auditing AMI and KMS key usage across accounts.
- Extending the automation so that each target account automatically:
- copies the source AMI’s snapshots,
- re-encrypts them with a target-account KMS key,
- and registers a new AMI in that account —
- This Gives each environment independent, and fully-owned AMIs.
- Because fully automated cross-account copying requires some cross-account bootstrap, twtech would love to provide two sample CloudFormation templates:
1. Target-account bootstrap — deploy this in each target account (Dev/QA/Prod).
- It creates a minimal IAM role (
AmiCopyRoleby default) the source-account orchestration can assume to perform EC2 snapshot-copy + register-image operations inside the target account.
- twtech (or the security team) can review & control this role in each target account.
2. Source-account orchestrator — deploy this in the source (producer) account. It:
- Creates the KMS key used for sharing in the source account (optional — twtech may already have one),
- Shares the snapshot(s) and AMI metadata with targets,
- Assumes the target role in each target account and copies every snapshot referenced by the source AMI into the target account using the target account’s KMS key(s),
- Registers a new AMI in the target account using the newly-copied snapshots.
Read First
- The automation supports AMIs with one or many EBS-backed snapshots (it discovers block-device mappings and copies each referenced snapshot).
- twtech must deploy the Target-account bootstrap in each target account beforehand (or otherwise ensure a role exists that the source account can assume).
- Each target
KMS key must allow the target
role to use it for
kms:Encrypt,kms:GenerateDataKey*,kms:DescribeKey, etc. (Key policy / grants live in the target account.) - This design assumes twtech wants the target accounts to own their copies; after copy completes, twtech can unshare the snapshot/AMI or revoke the KMS access the source granted.
- The Lambda code uses
assume_roleto act inside each target account — no secret sharing required.
Template A: Target-account bootstrap (deploy to each target account)
NB:
- Deploy in each target account to create an IAM role that allows the source-account orchestrator to assume and perform EC2 actions.
- Replace
SourceAccountIdwith the ID of the producer/source account.
AWSTemplateFormatVersion: '2010-09-09'Description: > Bootstrap role in target account that the source account's orchestrator will assume to copy snapshots and register AMIs into this account.Parameters: SourceAccountId: Type: String Description: AWS Account ID of the producer/source account (that will assume this role) RoleName: Type: String Default: AmiCopyRole Description: Name of role that will be assumed by source account orchestrator Resources: AmiCopyRole: Type: AWS::IAM::Role Properties: RoleName: twtechRef-RoleName AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: AWS: !Sub arn:aws:iam::${SourceAccountId}:root Action: sts:AssumeRole Condition: {} Description: Role assumed by producer account to copy snapshots and register AMIs in this target account. Policies: - PolicyName: twtechAmiCopyPolicy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:CopySnapshot - ec2:RegisterImage - ec2:DescribeSnapshots - ec2:DescribeImages - ec2:CreateTags - ec2:DescribeRegions Resource: "*" - Effect: Allow Action: - kms:Encrypt - kms:GenerateDataKey* - kms:DescribeKey - kms:CreateGrant Resource: "*" Outputs: TargetRoleArn: Description: ARN of the role the source orchestrator will assume in this account. Value: !Sub arn:aws:iam::${AWS::AccountId}:role/${RoleName}Security notes for template A (target
account)
- The role trust is intentionally limited to the source account root (twtech can substitute a narrower principal, e.g., a specific role ARN from the source account).
- The KMS actions in the inline policy are permissive (
Resource: "*") — they are included so operators can more easily use keys in the target account when following best-practice key policies that grant the role usage. In production you should bind KMS permissions to the specific target KMS key ARNs or require key policy grants instead of IAM-basedkms:*— prefer updating the target key policy to allow this role. (Key policy changes must be made in the target account.) - Installers should ensure the target KMS key policy explicitly allows the role to use the key (recommended).
Template B — Source-account orchestrator (deploy in the source/producer account)
This is the main stack. It:
- Shares the source snapshot(s) and AMI,
- Assumes the
AmiCopyRolein each target account, - Copies all referenced snapshots into the target account re-encrypting with the target KMS key specified,
- Registers a new AMI in the target account with the new snapshots.
Required
parameter inputs:
-
TargetAccountIds— Comma-delimited list of target account IDs (order must matchTargetKmsKeyIdsand you must have already deployed the target bootstrap in each account). -
TargetKmsKeyIds— Comma-delimited list of KMS Key ARNs or IDs in each target account (one entry per target account). These keys must exist in the target accounts and permit the target role to encrypt with them. -
TargetRoleName— the name of the role created by the Target bootstrap (defaults toAmiCopyRole).
AWSTemplateFormatVersion: '2010-09-09'Description: > Orchestrator in source account: shares an encrypted AMI/snapshots and automatically copies & re-encrypts them into each target account (via assume-role).Parameters: TargetAccountIds: Type: CommaDelimitedList Description: Comma-separated list of target account IDs (order must match TargetKmsKeyIds) TargetKmsKeyIds: Type: CommaDelimitedList Description: Comma-separated list of target KMS key ARNs or IDs for each target account (order must match TargetAccountIds) TargetRoleName: Type: String Default: AmiCopyRole Description: Name of the role in target accounts to assume (must match the bootstrap) AmiId: Type: String Description: Source AMI ID to share and copy Region: Type: String Default: us-east-2 Description: Region of source AMI and snapshots SourceSnapshotId: Type: String Description: One of the snapshot IDs backing the source AMI (this is used for sharing; full discovery is dynamic) LambdaTimeout: Type: Number Default: 300 Description: Lambda timeout in seconds (increase for large snapshot copies)Resources: ################################################## # 1) Optional: source KMS key (if twtech wants CFN to manage it) ################################################## SourceAmiKey: Type: AWS::KMS::Key Properties: Description: Optional CMK for the source AMI (managed by CFN); if twtech already has a key, ignore or pass existing key ARN in parameters manually (not wired). EnableKeyRotation: true KeyPolicy: Version: '2012-10-17' Statement: - Sid: twtechAdminAccess Effect: Allow Principal: AWS: !Sub arn:aws:iam::${AWS::AccountId}:root Action: 'kms:*' Resource: '*' ################################################## # 2) Lambda Role for Orchestration (assume-role & share) ################################################## SourceOrchestratorRole: Type: AWS::IAM::Role Properties: RoleName: !Sub SourceAmiOrchestratorRole-${AWS::StackName} AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/twtechAWSLambdaBasicExecutionRole Policies: - PolicyName: twtechSourceOrchestratorPolicy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:ModifySnapshotAttribute - ec2:ModifyImageAttribute - ec2:DescribeImages - ec2:DescribeSnapshots - ec2:DescribeRegions Resource: "*" - Effect: Allow Action: - sts:AssumeRole Resource: !Sub arn:aws:iam::*:role/${TargetRoleName} ################################################## # 3) Lambda that shares + assumes target role to copy & register AMI ################################################## OrchestratorLambda: Type: AWS::Lambda::Function Properties: Description: > Shares encrypted AMI/snapshots and copies & re-encrypts them into multiple target accounts. Handler: index.handler Runtime: python3.12 Timeout: !Ref LambdaTimeout Role: !GetAtt SourceOrchestratorRole.Arn Code: ZipFile: | import boto3, json, time import cfnresponse from botocore.exceptions import ClientError def assume(target_account, role_name): sts = boto3.client('sts') role_arn = f"arn:aws:iam::{target_account}:role/{role_name}" resp = sts.assume_role(RoleArn=role_arn, RoleSessionName='AmiCopySession') creds = resp['Credentials'] return creds def wait_for_snapshot_available(ec2_client, snapshot_id, max_wait_minutes=60): wait_seconds = 10 elapsed = 0 while True: resp = ec2_client.describe_snapshots(SnapshotIds=[snapshot_id]) state = resp['Snapshots'][0]['State'] if state == 'completed': return True if state == 'error': raise Exception(f"Snapshot {snapshot_id} in error state") time.sleep(wait_seconds) elapsed += wait_seconds if elapsed > max_wait_minutes*60: raise Exception("Timed out waiting for snapshot to complete") def handler(event, context): print("Event:", json.dumps(event)) props = event.get('ResourceProperties', {}) target_accounts = [a.strip() for a in props.get('TargetAccountIds','').split(',') if a.strip()] target_keys = [k.strip() for k in props.get('TargetKmsKeyIds','').split(',') if k.strip()] role_name = props.get('TargetRoleName','AmiCopyRole') ami_id = props['AmiId'] region = props.get('Region','us-east-1') src_snapshot_hint = props.get('SourceSnapshotId') if len(target_accounts) != len(target_keys): cfnresponse.send(event, context, cfnresponse.FAILED, {"Error":"TargetAccountIds and TargetKmsKeyIds length mismatch"}) return ec2_src = boto3.client('ec2', region_name=region) try: # 1) Share snapshot hint and AMI with target accounts (so they can describe/copy) for account in target_accounts: try: ec2_src.modify_snapshot_attribute( SnapshotId=src_snapshot_hint, Attribute='createVolumePermission', OperationType='add', UserIds=[account] ) except ClientError as e: print("Snapshot share error (may already be shared):", e) try: ec2_src.modify_image_attribute( ImageId=ami_id, LaunchPermission={'Add':[{'UserId': account}]} ) except ClientError as e: print("AMI share error (may already be shared):", e) # 2) Discover the AMI block-device mappings (including all source snapshot IDs) desc = ec2_src.describe_images(ImageIds=[ami_id])['Images'][0] block_mappings = desc.get('BlockDeviceMappings', []) # Map of old_snapshot_id -> new_snapshot_id per target source_snap_ids = [] for b in block_mappings: ebs = b.get('Ebs') if ebs and 'SnapshotId' in ebs: source_snap_ids.append(ebs['SnapshotId']) # If snapshot list empty, fallback to provided hint if not source_snap_ids and src_snapshot_hint: source_snap_ids = [src_snapshot_hint] results = [] # For each target account, assume role and copy each snapshot, then register AMI for idx, target in enumerate(target_accounts): target_key = target_keys[idx] print(f"Processing target {target} using target KMS key {target_key}") creds = assume(target, role_name) # Create target-account EC2 client using assumed credentials ec2_tgt = boto3.client('ec2', region_name=region, aws_access_key_id=creds['AccessKeyId'], aws_secret_access_key=creds['SecretAccessKey'], aws_session_token=creds['SessionToken'] ) new_snapshot_map = {} # Copy each source snapshot into target account for src_snap in source_snap_ids: print(f"Copying snapshot {src_snap} to target {target}") copy_resp = ec2_tgt.copy_snapshot( SourceRegion=region, SourceSnapshotId=src_snap, Description=f"Copy of {src_snap} from {boto3.client('sts').get_caller_identity()['Account']}", KmsKeyId=target_key ) new_snap_id = copy_resp['SnapshotId'] print("Started copy job, new snapshot id:", new_snap_id) # Wait for copy to complete wait_for_snapshot_available(ec2_tgt, new_snap_id) new_snapshot_map[src_snap] = new_snap_id # Build block device mapping for register_image call using new snapshot ids new_block_mappings = [] for b in block_mappings: device_name = b.get('DeviceName') ebs = b.get('Ebs') if ebs and 'SnapshotId' in ebs: old = ebs['SnapshotId'] new = new_snapshot_map.get(old) if not new: raise Exception(f"Missing new snapshot mapping for {old}") ebs_new = { 'SnapshotId': new, # copy through volume size / deleteOnTermination if present 'DeleteOnTermination': ebs.get('DeleteOnTermination', True), 'VolumeSize': ebs.get('VolumeSize'), 'VolumeType': ebs.get('VolumeType') } new_block_mappings.append({ 'DeviceName': device_name, 'Ebs': {k:v for k,v in ebs_new.items() if v is not None} }) else: # Non-EBS mapping (e.g., ephemeral) - copy as-is new_block_mappings.append(b) # Choose a name for the new AMI in target account new_ami_name = f"{desc.get('Name','shared-ami')}-copied-from-{boto3.client('sts').get_caller_identity()['Account']}" reg_resp = ec2_tgt.register_image( Name=new_ami_name, Architecture=desc.get('Architecture'), BlockDeviceMappings=new_block_mappings, RootDeviceName=desc.get('RootDeviceName'), VirtualizationType=desc.get('VirtualizationType'), Description=f"Copy of AMI {ami_id} from account {boto3.client('sts').get_caller_identity()['Account']}" ) new_ami_id = reg_resp['ImageId'] print(f"Registered new AMI {new_ami_id} in account {target}") results.append({'TargetAccount': target, 'NewAmiId': new_ami_id, 'SnapshotMap': new_snapshot_map}) cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Results': results}) except Exception as e: import traceback print("Error during orchestration:", e) traceback.print_exc() cfnresponse.send(event, context, cfnresponse.FAILED, {'Error': str(e)}) ################################################## # 4) Custom Resource to invoke the orchestrator ################################################## OrchestratorInvoke: Type: Custom::OrchestrateAmiCopy Properties: ServiceToken: !GetAtt OrchestratorLambda.Arn TargetAccountIds: !Join [",", !Ref TargetAccountIds] TargetKmsKeyIds: !Join [",", !Ref TargetKmsKeyIds] TargetRoleName: !Ref TargetRoleName AmiId: !Ref AmiId Region: !Ref Region SourceSnapshotId: !Ref SourceSnapshotIdOutputs: OrchestrationResult: Description: Orchestration result (list of new AMIs per target account) Value: !GetAtt OrchestratorInvokeHow to use these templates (deployment steps)
Step-1. Deploy
Target bootstrap in each target account (one per account)
In Target Account
A (Dev):
aws cloudformation deploy \ --template-file twtechtarget-bootstrap.yaml \ --stack-name twtechAmiCopyRoleBootstrap \ --parameter-overrides SourceAccountId=111111111111NB:
- The
TargetRoleArnoutput — that role will be assumed by the source orchestration.
Step-2. Ensure
KMS keys exist in each target and allow the role
o
Create or identify a KMS key in each target
account and update the KMS key policy to allow the AmiCopyRole to use it:
§ Actions:
kms:Encrypt, kms:GenerateDataKey*, kms:DescribeKey, kms:CreateGrant (or create an explicit grant).
§ Scope
as tightly as possible to the role ARN and kms:ViaService
= ec2.<region>.amazonaws.com.
Step-3. Deploy
Orchestrator in the source account
# Sample:
aws cloudformation deploy \ --template-file twtechsource-orchestrator.yaml \ --stack-name AmiShareOrchestrator \ --parameter-overrides \ TargetAccountIds=222222222222,333333333333 \ TargetKmsKeyIds=arn:aws:kms:us-east-2:222222222222:key/twtechkmskey-aaaa-...,arn:aws:kms:us-east-2:333333333333:key/twtechkmskey-bbbb-... \ AmiId=ami-0abcd1234ef567890 \ SourceSnapshotId=snap-0abc123456789def0 \ Region=us-east-24. Verify
o
In each target account, describe images: aws ec2 describe-images --owners self
and you should see a new AMI registered (named
like ...-copied-from-<source-account>).
o
Test launching an instance from the new AMI in
each target account.
5. Post-copy
cleanup (optional but recommended)
o
Once targets have successfully copied &
re-encrypted their snapshots, you can:
§ Remove
the target account from the source KMS key policy.
§ Remove
the source snapshot createVolumePermission for that target.
§ Remove
AMI launch permission if you no longer want live sharing.
Limitations & caveats
Permissions must be correct in all accounts:
- The Source Orchestrator needs permission to
share the snapshot and AMI (it has
those).
- The TargetRole must be created in each target account and must allow the source account to assume it.
- The target KMS key policy must allow that role to encrypt / generate data keys.
Large snapshots take time
- copying snapshots may take many minutes/hours depending on data size and AWS internals.
- Lambda timeout may need to be increased, or twtech should implement an asynchronous Step Function or SNS callback.
- The template’s
LambdaTimeoutdefault is 300s — - increase for large data or convert to Step Functions.
Edge cases:
- AMIs with non-EBS / instance-store volumes are copied only by preserving the mapping —
- instance-store cannot be copied as snapshot.
- If the source AMI has snapshots across multiple
regions, adjust
SourceRegionappropriately per snapshot (this template assumes single-region).
Security:
- twtech Reviews all IAM and KMS policies to enforce least privilege.
- It may Prefer making KMS key policy changes
in the target account (grant the target
role explicitly rather than relying on IAM
kms:*)
No comments:
Post a Comment