Back to blog
AWSBest PracticesCloud SecurityCompute & ContainersIdentity & Access

ECR Repository Not Using a Customer-Managed KMS Key

Learn why ECR repositories should use a customer-managed KMS key instead of the AWS-managed default, the risks involved, and step-by-step remediation.

TL;DR

This check flags ECR repositories encrypted with the default AWS-managed key instead of a customer-managed KMS key (CMK). Without a CMK you lose control over the key policy, rotation, and access auditing. Fix it by creating a repository with --encryption-configuration encryptionType=KMS,kmsKey=<arn> and migrating your images.

Container images are code. They often bundle proprietary application logic, internal dependencies, configuration baked into layers, and sometimes secrets that should never have been committed in the first place. Amazon Elastic Container Registry (ECR) encrypts every repository at rest by default, but the default option uses an AWS-managed key that you cannot see, control, or restrict. This check, ecr_nocmk, identifies repositories that rely on that default instead of a customer-managed KMS key.


What this check detects

Every ECR repository has an encryption configuration set at creation time. There are two modes:

  • AES256 — server-side encryption with an Amazon S3-managed key. This is the legacy default and offers no key visibility at all.
  • KMS — encryption with a KMS key. If you do not specify a key, ECR uses the AWS-managed aws/ecr key. If you specify your own key ARN, it uses a customer-managed CMK.

The ecr_nocmk check fires when a repository uses either AES256 or the AWS-managed aws/ecr key rather than a CMK that you own and govern.

Note: The AWS-managed key (aws/ecr) is not visible in your KMS key list as a customer key, you cannot edit its key policy, and you cannot disable or schedule it for deletion. AWS rotates it on its own schedule. A CMK gives you all of those controls back.


Why it matters

Default encryption protects you against one narrow threat: someone walking off with the physical disk. That is genuinely useful, but it is not the threat most teams actually face. The real questions are about access control and auditability, and that is exactly where the AWS-managed key falls short.

You cannot enforce who decrypts your images

With a CMK, the key policy becomes a second layer of authorization. Even if an IAM principal has ecr:GetDownloadUrlForLayer and ecr:BatchGetImage, they still need kms:Decrypt on your CMK to actually pull and read the image layers. That means a compromised CI token or an over-permissive role does not automatically translate into the ability to exfiltrate your container images. With the AWS-managed key, ECR permissions alone are enough.

You lose granular audit trails

Operations against a CMK show up in CloudTrail with the key ARN, the calling principal, and the encryption context. When you are investigating who pulled a sensitive image and when, a CMK gives you a clean, attributable record. The AWS-managed key produces far less useful data for forensic work.

Warning: Many regulatory frameworks (PCI DSS, HIPAA, FedRAMP, and various SOC 2 controls) expect you to demonstrate control over your own encryption keys, including documented rotation and access policies. An AWS-managed key makes that story much harder to tell during an audit.

No cross-account control without a CMK

If you share repositories across accounts, for example a central registry that build accounts push to and runtime accounts pull from, you cannot grant the AWS-managed key to other accounts. A CMK key policy lets you explicitly allow specific external principals to decrypt, keeping cross-account access tight and intentional.


How to fix it

The catch with ECR encryption is that you cannot change it after the repository is created. Encryption configuration is immutable. Fixing an existing repository means creating a new one with the correct configuration and migrating images across.

Step 1: Create or pick a CMK

Create a dedicated KMS key for ECR. Using a separate key per workload or per environment keeps blast radius small and key policies readable.

aws kms create-key \
  --description "CMK for ECR repositories - prod" \
  --tags TagKey=team,TagValue=platform TagKey=purpose,TagValue=ecr

# Give it a friendly alias
aws kms create-alias \
  --alias-name alias/ecr-prod \
  --target-key-id <key-id-from-previous-command>

Step 2: Scope the key policy

Attach a key policy that grants only the principals that actually push and pull. Here is a starting point that allows account admins to manage the key and a specific CI role plus a runtime role to use it:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowKeyAdmins",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111122223333:role/kms-admin" },
      "Action": "kms:*",
      "Resource": "*"
    },
    {
      "Sid": "AllowECRUse",
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::111122223333:role/ci-push",
          "arn:aws:iam::111122223333:role/eks-node-pull"
        ]
      },
      "Action": [
        "kms:Decrypt",
        "kms:GenerateDataKey",
        "kms:DescribeKey"
      ],
      "Resource": "*"
    }
  ]
}

Step 3: Create the repository with the CMK

aws ecr create-repository \
  --repository-name my-app \
  --image-scanning-configuration scanOnPush=true \
  --encryption-configuration encryptionType=KMS,kmsKey=arn:aws:kms:us-east-1:111122223333:key/<key-id>

Step 4: Migrate images from the old repository

Pull each tag from the old repo and push it to the new one. A quick loop handles a repository with multiple tags:

OLD=111122223333.dkr.ecr.us-east-1.amazonaws.com/my-app-legacy
NEW=111122223333.dkr.ecr.us-east-1.amazonaws.com/my-app

aws ecr get-login-password --region us-east-1 \
  | docker login --username AWS --password-stdin 111122223333.dkr.ecr.us-east-1.amazonaws.com

for TAG in $(aws ecr list-images --repository-name my-app-legacy \
  --query 'imageIds[].imageTag' --output text); do
  docker pull "$OLD:$TAG"
  docker tag "$OLD:$TAG" "$NEW:$TAG"
  docker push "$NEW:$TAG"
done

Tip: If you want to preserve image digests exactly rather than rebuilding manifests, use crane copy from Google's go-containerregistry. It copies the original layers and manifest byte for byte, so digests stay identical: crane copy $OLD:$TAG $NEW:$TAG.

Step 5: Repoint consumers, then delete the old repository

Update your task definitions, Helm values, Kubernetes manifests, and pipelines to reference the new repository. Confirm everything pulls cleanly before removing the old one.

Danger: Deleting a repository with --force permanently destroys every image inside it. Verify that all consumers are pulling from the new CMK-backed repository and that you have no rollback deployments still pinned to old tags before you run this.

aws ecr delete-repository \
  --repository-name my-app-legacy \
  --force

How to prevent it from happening again

Manual fixes do not scale, and the next repository someone creates through the console will default right back to AES256. Bake the CMK requirement into your provisioning workflow.

Terraform

Define repositories with explicit KMS encryption and never let anyone create one by hand:

resource "aws_kms_key" "ecr" {
  description         = "CMK for ECR - ${var.env}"
  enable_key_rotation = true
}

resource "aws_ecr_repository" "app" {
  name                 = "my-app"
  image_tag_mutability = "IMMUTABLE"

  encryption_configuration {
    encryption_type = "KMS"
    kms_key         = aws_kms_key.ecr.arn
  }

  image_scanning_configuration {
    scan_on_push = true
  }
}

Policy-as-code gate

Add a check to your plan stage that fails CI if a repository is missing KMS encryption. With OPA/Conftest against a Terraform plan:

package terraform.ecr

deny[msg] {
  r := input.resource_changes[_]
  r.type == "aws_ecr_repository"
  enc := r.change.after.encryption_configuration[_]
  enc.encryption_type != "KMS"
  msg := sprintf("ECR repo '%s' must use KMS encryption", [r.change.after.name])
}

Tip: Pair this with an SCP that denies ecr:CreateRepository unless the request includes a KMS encryption type. That stops console and ad hoc CLI creation from bypassing your IaC entirely, which is the most common way these drift in.


Best practices

  • Enable key rotation on your CMK (enable_key_rotation = true). It is automatic, free, and removes a manual chore.
  • Scope key policies tightly. Grant kms:Decrypt only to the roles that pull images and kms:GenerateDataKey only to those that push. Avoid wildcards on principals.
  • Use immutable tags. Combine CMK encryption with IMMUTABLE tags so an attacker cannot overwrite a known-good tag with a malicious image.
  • Separate keys by environment. Distinct keys for prod, staging, and dev keep blast radius contained and make access reviews clearer.
  • Turn on scan-on-push. Encryption protects the image at rest; vulnerability scanning protects what is inside it. Run both.
  • Watch CloudTrail for the new key. Now that decrypt events are attributable, alert on unexpected principals calling kms:Decrypt against your ECR key.

The cost is minimal: one KMS key runs about a dollar a month plus per-request charges that are negligible for normal pull volumes. For that you get key policy enforcement, clean audit trails, and a compliance story that holds up. There is no good reason to leave production registries on the default key.