Skip to content

CodePipeline artifact store encryption — the KMS trust block that breaks without kms:ViaService

Customer-managed KMS keys for the artifact bucket, cross-account key sharing for shared pipelines, bucket policies that block unencrypted uploads, key rotation. The kms:ViaService condition you cannot skip.

An auditor flags the CodePipeline artifact bucket: encrypted at rest, but with the default AWS-managed aws/s3 key. The bucket has 18 months of build artifacts — every commit, every test run, every deploy artifact — and the audit wants either deletion of older content, customer-managed keys, or a documented exception. The team wants customer-managed keys, but the first attempt to switch breaks the pipeline because the IAM trust block on the new key is missing the kms:ViaService condition that lets CodePipeline’s service role use the key. Here is the configuration that gets through the audit and keeps the pipeline running.

What’s happening

The CodePipeline artifact store has three independent encryption decisions:

  • The S3 bucket’s server-side encryption. This is set at the bucket level — either SSE-S3 (AES-256, AWS-managed) or SSE-KMS (KMS-encrypted with an AWS-managed or customer-managed key). For audit purposes, customer-managed is what gets the green check.
  • The pipeline’s EncryptionKey setting. This is a separate field on the pipeline definition — ArtifactStore.EncryptionKey references the KMS key CodePipeline will use to encrypt the artifact metadata it writes to S3. It defaults to the AWS-managed aws/s3 key.
  • Cross-account access. If the pipeline lives in a separate AWS account from the build or deploy target, the KMS key policy must allow the cross-account role to use the key — and the bucket policy must allow the cross-account role to read/write.

The audit failure mode that everyone hits is the third one: the pipeline encrypts artifacts with a customer-managed key in the pipeline account, but the deploy stage runs in a different account and the deploy role does not have kms:Decrypt on the pipeline-account key. The deploy action fails with AccessDenied, and the temptation is to broaden the key policy with Principal: '*' and kms:*, which fails the next audit harder.

The correct fix is a specific key policy with kms:ViaService condition restricting the cross-account access to specifically the CodePipeline service, plus a bucket policy that blocks any upload not encrypted with that key.

How to verify

The customer-managed key, with the trust block that does and does not work:

Broken (overprivileged):

{
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "AWS": "arn:aws:iam::222222222222:root" },
    "Action": "kms:*",
    "Resource": "*"
  }]
}

This passes the smoke test (cross-account works) and fails the audit (any role in account 222222222222 can use the key for anything, including direct S3 decrypts that bypass CodePipeline).

Correct (least-privilege with kms:ViaService):

{
  "Statement": [
    {
      "Sid": "PipelineAccountRoot",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::111111111111:root" },
      "Action": "kms:*",
      "Resource": "*"
    },
    {
      "Sid": "DeployAccountCodePipelineOnly",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::222222222222:role/CrossAccountDeployRole" },
      "Action": [
        "kms:Decrypt",
        "kms:DescribeKey",
        "kms:GenerateDataKey"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "kms:ViaService": "s3.ca-central-1.amazonaws.com"
        },
        "StringLike": {
          "kms:EncryptionContext:aws:s3:arn": "arn:aws:s3:::pipeline-artifacts-ca-central-1/*"
        }
      }
    }
  ]
}

The kms:ViaService condition forces the cross-account principal to use the key only via S3 — direct kms:Decrypt calls (like a leaked role credential trying to read a downloaded artifact ciphertext) fail. The kms:EncryptionContext:aws:s3:arn condition further restricts to specifically the pipeline artifact bucket, not any bucket the deploy role can touch.

The bucket policy enforces the encryption from the storage side:

{
  "Statement": [
    {
      "Sid": "DenyUnencryptedUploads",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::pipeline-artifacts-ca-central-1/*",
      "Condition": {
        "StringNotEquals": {
          "s3:x-amz-server-side-encryption": "aws:kms"
        }
      }
    },
    {
      "Sid": "DenyWrongKey",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::pipeline-artifacts-ca-central-1/*",
      "Condition": {
        "StringNotLikeIfExists": {
          "s3:x-amz-server-side-encryption-aws-kms-key-id": "arn:aws:kms:ca-central-1:111111111111:key/abc-123-def-456"
        }
      }
    }
  ]
}

The two Deny statements together mean: every object in the bucket must be KMS-encrypted, and specifically with the right key. An attempt to upload with the AWS-managed aws/s3 key or no encryption fails before the object lands.

Verify the bucket-level encryption is enforced:

aws s3api get-bucket-encryption \
  --bucket pipeline-artifacts-ca-central-1

Expected output references the customer-managed key ARN, not AES256.

Verify the pipeline references the same key:

aws codepipeline get-pipeline \
  --name app-pipeline \
  --query 'pipeline.artifactStore.encryptionKey'

Expected output: {"id": "arn:aws:kms:ca-central-1:111111111111:key/abc-123-def-456", "type": "KMS"}.

If the pipeline’s encryptionKey is null (which is the CloudFormation default), CodePipeline uses the AWS-managed key — and the bucket policy’s “wrong key” Deny will block the next pipeline run. Fix the pipeline definition before adding the bucket policy.

The fix

The artifact encryption setup, in order:

  1. Create a customer-managed KMS key in the pipeline’s region. Tag it purpose=codepipeline-artifacts. Set EnableKeyRotation: true — yearly automatic rotation is supported for symmetric customer-managed keys and is the audit-default expectation.

  2. Write the key policy with kms:ViaService for any cross-account principal. Pipeline-account root gets kms:* (so administrators can manage it). Every other principal gets a narrow kms:Decrypt, kms:DescribeKey, kms:GenerateDataKey with the kms:ViaService: s3.<region>.amazonaws.com condition. Without kms:ViaService, the key can be used directly for decrypt by anyone with kms:Decrypt — that is the vulnerability the audit will find on the next pass.

  3. Update the pipeline’s ArtifactStore.EncryptionKey. In the CloudFormation, set EncryptionKey: { Id: !GetAtt ArtifactKey.Arn, Type: KMS }. Without this, CodePipeline writes with the default key and the new bucket policy will block the next run.

  4. Apply the bucket policy. The two Deny statements enforce both “must be KMS-encrypted” and “must be the right KMS key.” Apply in that order — if you apply the wrong-key Deny first while the bucket still has objects encrypted with the wrong key, listing operations break.

  5. For cross-account deploys, grant kms:Decrypt to the deploy-account role with kms:ViaService. The deploy-account role assumes from the pipeline-account role, so the trust chain is: pipeline service role → cross-account deploy role → S3 GetObject (which calls KMS Decrypt under the hood). Each link needs the right permission; the kms:ViaService keeps the deploy role from being able to decrypt anything other than via S3.

  6. Verify the existing artifacts. Old artifacts in the bucket may have been written with the AWS-managed key. The bucket policy denies new wrong-key uploads, but does not retroactively re-encrypt. For audit purposes, either delete artifacts older than the retention window (S3 lifecycle policy) or re-encrypt them with aws s3 cp s3://bucket/key s3://bucket/key --sse aws:kms --sse-kms-key-id <new-key>.

Three rules that hold across every artifact-encryption engagement:

Customer-managed key, always, for production pipelines. AWS-managed aws/s3 is fine for personal projects, not for a SaaS that will face SOC 2 or HIPAA review.

kms:ViaService on every cross-account grant. Without it, a leaked deploy-account credential becomes a “read all pipeline artifacts ever” problem. With it, the credential only works via S3, which is what the design intends.

Key rotation is automatic, not manual. Enable rotation at key creation time. CodePipeline handles re-encryption transparently when the key rotates; manual rotation in production tends to find every place the key ID was hard-coded that the team forgot about.

The audit pass is the easy part once the configuration is right. The hard part is getting the trust block right the first time.


Stack Harbor builds and operates production CodePipeline workflows as part of Managed AWS onboarding and the ongoing Code Deploy Pipeline operating layer. If your release path is still SSH + a Slack message, start with an infrastructure consult.