Cross account deployments using a Customer Managed KMS key
Tags: crossaccount, customermanaged, kms, codepipeline
Published: 2023-02-27 10:48:52

In my previous [blog](https://xebia.com/blog/building-applications-with-pipelines/) I wrote about application pipelines. These CodePipeline use a S3 bucket. What if you have a need for a customer managed key instead of the Amazon managed key? And you want to deploy the CloudFormation templates cross account? In this blog I will explain how you can achieve this.

## The key itself

Lets start with the key itself. When CodePipeline moves artifacts from one stage to another it uses S3 to transfer the data. For this reason we need to allow S3 and CodePipeline to decrypt and encrypt the data:

```yaml
- Action:
    - kms:Decrypt
    - kms:DescribeKey
    - kms:Encrypt
    - kms:ReEncrypt*
    - kms:GenerateDataKey*
  Effect: Allow
  Principal:
    AWS: "*"
  Resource: "*"
  Condition:
    StringEquals:
      aws:PrincipalOrgID: !Ref OrgId
      kms:ViaService:
        - Fn::Sub: s3.${AWS::Region}.amazonaws.com
        - Fn::Sub: codepipeline.${AWS::Region}.amazonaws.com
```

Next, CloudFormation will deploy these templates across account. For this reason we need to allow CloudFormation to decrypt the data.

```yaml
- Action: kms:Decrypt
  Effect: Allow
  Principal:
    AWS: "*"
  Resource: "*"
  Condition:
    StringEquals:
      aws:PrincipalOrgID: !Ref OrgId
      kms:ViaService: !Sub cloudformation.${AWS::Region}.amazonaws.com
```

The samples shown here will limit the use of the key to the AWS Services within your own AWS organization. The S3 bucket needs to use the KMS key for encryption.

## The pipeline

The CodePipeline needs to know that we are using KMS as an encryption method. For this reason we need to tell what key to use.

```yaml
Pipeline:
  Type: AWS::CodePipeline::Pipeline
  Properties:
    ArtifactStores:
      - Region: eu-west-1
        ArtifactStore:
          Location: !Sub codepipeline-artifacts-${AWS::AccountId}-eu-west-1
          Type: S3
          EncryptionKey:
            Type: KMS
            Id: alias/bucket-key
      - Region: eu-central-1
        ArtifactStore:
          Location: !Sub codepipeline-artifacts-${AWS::AccountId}-eu-central-1
          Type: S3
          EncryptionKey:
            Type: KMS
            Id: alias/bucket-key
```

We are using the alias so that we do not have to deal with the key id.

## Deploy cross account/region

You might have seen it already in the previous code snippet. When you deploy across region you need to configure a bucket and key per region. When you deploy across account you will need 2 roles.

1.  A role to assume in the other account. (cross-account-role, configured on action level)
2.  A role to use for the CloudFormation execution. (cloudformation-execution-role, configured in the configuration)

```yaml
Actions:
  - Name: Ireland-CreateChangeSet
    Region: eu-west-1
    RunOrder: 1
    RoleArn: !Sub arn:aws:iam::${DevelopmentAccountId}:role/cross-account-role
    InputArtifacts:
      - Name: BuildArtifactAsZip
    ActionTypeId:
      Category: Deploy
      Owner: AWS
      Provider: CloudFormation
      Version: "1"
    Configuration:
      ActionMode: CHANGE_SET_REPLACE
      RoleArn: !Sub arn:aws:iam::${DevelopmentAccountId}:role/cloudformation-execution-role
      StackName: !Sub ${ProjectName}-$development
      ChangeSetName: !Sub ${ProjectName}-development-ChangeSet
      TemplatePath: BuildArtifactAsZip::packaged-template.yaml
      Capabilities: CAPABILITY_NAMED_IAM
      ParameterOverrides: |
        {
          "EnvType": "development"
        }
  - Name: Ireland-ExecuteChangeSet
    Region: eu-west-1
    RunOrder: 2
    RoleArn: !Sub arn:aws:iam::${DevelopmentAccountId}:role/cross-account-role
    ActionTypeId:
      Category: Deploy
      Owner: AWS
      Provider: CloudFormation
      Version: "1"
    Configuration:
      ActionMode: CHANGE_SET_EXECUTE
      RoleArn: !Sub arn:aws:iam::${DevelopmentAccountId}:role/cloudformation-execution-role
      StackName: !Sub ${ProjectName}-development
      ChangeSetName: !Sub ${ProjectName}-development-ChangeSet
```

The cross-account-role needs the ability to perform crud operations on CloudFormation. Have access to the templates and the ability to pass the cloudformation-execution-role. The `BuildAccountId` is the account id that holds the KMS keys, S3 buckets and pipeline. This role needs to be able to access the S3 buckets and use the KMS key for decryption.

```yaml
CrossAccountRole:
  Type: AWS::IAM::Role
  Properties:
    RoleName: cross-account-role
    AssumeRolePolicyDocument:
      Version: 2012-10-17
      Statement:
        - Action: sts:AssumeRole
          Effect: Allow
          Principal:
            AWS: !Sub arn:aws:iam::${BuildAccountId}:root
    Policies:
      - PolicyName: AllowCloudFormationDeployments
        PolicyDocument:
          Version: 2012-10-17
          Statement:
            - Effect: Allow
              Action: iam:PassRole
              Resource: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cloudformation-execution-role
            - Effect: Allow
              Action: cloudformation:DescribeStacks
              Resource:
                - !Sub arn:${AWS::Partition}:cloudformation:eu-west-1:${AWS::AccountId}:stack/*
                - !Sub arn:${AWS::Partition}:cloudformation:eu-central-1:${AWS::AccountId}:stack/*
            - Effect: Allow
              Action:
                - cloudformation:CreateStack
                - cloudformation:DeleteStack
                - cloudformation:UpdateStack
                - cloudformation:CreateChangeSet
                - cloudformation:ExecuteChangeSet
                - cloudformation:DeleteChangeSet
                - cloudformation:DescribeChangeSet
                - cloudformation:SetStackPolicy
                - cloudformation:SetStackPolicy
                - cloudformation:ValidateTemplate
              Resource:
                - !Sub arn:${AWS::Partition}:cloudformation:eu-west-1:${AWS::AccountId}:stack/*
                - !Sub arn:${AWS::Partition}:cloudformation:eu-central-1:${AWS::AccountId}:stack/*
      - PolicyName: AllowArtifactAccess
        PolicyDocument:
          Version: 2012-10-17
          Statement:
            - Effect: Allow
              Action:
                - s3:GetObject
                - s3:GetObjectVersion
              Resource:
                - !Sub arn:${AWS::Partition}:s3:::codepipeline-artifacts-${BuildAccountId}-eu-west-1/*
                - !Sub arn:${AWS::Partition}:s3:::codepipeline-artifacts-${BuildAccountId}-eu-central-1/*
            - Effect: Allow
              Action: kms:Decrypt
              Resource:
                - !Sub arn:${AWS::Partition}:kms:eu-west-1:${BuildAccountId}:key/02d730ed-125b-4d2e-a498-7b0187298924
                - !Sub arn:${AWS::Partition}:kms:eu-central-1:${BuildAccountId}:key/e127b617-6951-42b7-83ae-12acc16d12a7

```

The execution role has all the needed rights to deploy the actual template:

```yaml
CloudFormationServiceRole:
  Properties:
    RoleName: cloudformation-execution-role
    AssumeRolePolicyDocument:
      Version: 2012-10-17
      Statement:
        - Action: sts:AssumeRole
          Effect: Allow
          Principal: { Service: cloudformation.amazonaws.com }
    ManagedPolicyArns:
      - !Sub arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess
```

## Conclusion

When using customer managed keys you need some extra configuration. The key is self, configuration where is is used and some logical permissions. Getting all this from the AWS documentation can be challenging as it is somewhat of an edge case.