DEV Community

Cover image for DevSecOps with AWS- Integrate Azure DevOps for CDK deployments Part-1
Alejandro Velez for AWS Community Builders

Posted on • Updated on

DevSecOps with AWS- Integrate Azure DevOps for CDK deployments Part-1

Level 300

Table of Contents

There are many tools and services for CI/CD pipelines and possibilities to integrate them with AWS Cloud. The challenge is keeping the flexibility without lost the security and limit the operational overload. In this series you’ll find a common scenario that easily you can extrapolate as pattern to manage the identity for these kinds of integrations.

Imagine that you are a DevSecOps engineer and need to integrate Azure DevOps securely to your AWS Cloud environment, you need to have a single point to manage the identity, apply the least privilege principle and don’t affect the current deployments and account setups that currently your developers’ teams had been doing with CDK.

Hands On

Requirements

  • cdk >= 2.60.0
  • AWS CLI >= 2.7.0
  • Python >= 3.10.4
  • Pytest >= 7.1.3
  • cdk-nag >=2.18.44
  • checkov >= 2.1.229
  • AWS Toolkit for Azure DevOps
  • Azure DevOps Account

AWS Services and tools

  • AWS Cloud Development Kit (CDK): is an open-source software development framework to define your cloud application resources using familiar programming languages.
  • AWS Identity and Access Management (IAM): Securely manage identities and access to AWS services and resources.
  • AWS IAM Identity Center (Successor to AWS Single Sign-On): helps you securely create or connect your workforce identities and manage their access centrally across AWS accounts and applications.
  • AWS Key Management Service (AWS KMS): lets you create, manage, and control cryptographic keys across your applications and more than 100 AWS services.
  • AWS CloudFormation: Speed up cloud provisioning with infrastructure as code as code

  • AWS Security Token Service: web service that enables you to request temporary, limited-privilege credentials for AWS Identity and Access Management (IAM) users or for users you authenticate (federated users).

  • AWS Secrets Manager: a secrets management service that helps you protect access to your applications, services, and IT resources. This service enables you to easily rotate, manage, and retrieve database credentials, API keys, and other secrets throughout their lifecycle.

  • AWS Toolkit for Azure DevOps: adds tasks to easily enable build and release pipelines in Azure DevOps (formerly VSTS) and Azure DevOps Server (previously known as Team Foundation Server (TFS)) to work with AWS services including Amazon S3, AWS Elastic Beanstalk, AWS CodeDeploy, AWS Lambda, AWS CloudFormation, Amazon Simple Queue Service and Amazon Simple Notification Service, and run commands using the AWS Tools for Windows PowerShell module and the AWS CLI.

Solution Overview

Conditions:

  • Restrict time range for lifetime user credentials.
  • Apply context conditions to improve and reduce risk.
  • Use IaC.
  • Don’t affect current setup.

Visit DevSecOps with AWS - Multi Environment deployments - Part 1 for bootstrapping accounts using CDK. 👈

The figure 1. Depicts the solution architecture.

Azure DevOps and AWS Cloud
Figure 1. Solution Architecture

First, the service connection is created in Azure DevOps through AWS Toolkit, using access key, secret access key, external id, ARN role for assuming for any AWS task and session name.

Second, for a task that requires AWS access use the service connection credentials.

Keep in mind that these credentials are temporary, and the duration session is one hour.

Third, the CDK command assume role for target account base on the environment properties in the CDK application.

Finally, the app is deployed in target account.

Step by Step

The CDK project

It’s time to code 💻 .
As is usual, for this setup the automation was created with CDK using python. You can find de code here.

GitHub logo velez94 / cdkv2_prog_user_deploy

Project for creating a programmatic user for integrate CI/CD tools for cdk deployments

Table of Contents

DevSecOps with AWS- Integrate Azure DevOps for CDK deployments

Please Visit DevSecOps with AWS- Integrate Azure DevOps for CDK deployments Part-1

Level 300

There are many tools and services for CI/CD pipelines and possibilities to integrate them with AWS Cloud. The challenge is keeping the flexibility without lost the security and limit the operational overload. In this series you’ll find a common scenario that easily you can extrapolate as pattern to manage the identity for these kinds of integrations.

Imagine that you are a DevSecOps engineer and need to integrate Azure DevOps securely to your AWS Cloud environment, you need to have a single point to manage the identity, apply the least privilege principle…

Project parameters

All project definitions are in project_configs/environment_options/environment_options.yaml


project_name: "AzureDevOpsUser"
# Repository definitions
repository_properties:
  repository_name: "cdkv2_prog_user_deploy"
  create_repository: "true"
  description: "Repository for azure devops cdk user"
  branch: "master"

# Multi Environment setup
devsecops_account: "123456789012"
devsecops_region: "us-east-2"

deployment_account: "123456789012"
deployment_region: "us-east-2"

stg_account: "123456789014"
stg_region: "us-east-2"

iam_properties:
  user_name: "azdp_lab"
  context:
    from: "2023-01-01T00:00:00Z"
    up: "2023-06-30T23:59:59Z"
  assume_roles:
    - arn: "arn:aws:iam::123456789014:role/cdk-hnb659fds-deploy-role-123456789014-us-east-2"
    - arn: "arn:aws:iam::123456789014:role/cdk-hnb659fds-lookup-role-123456789014-us-east-2"


# Tags definitions align with corporation instructions
tags:
  - key: "Project"
    value: "AzureDevOpsUser"
  - key: "Environment"
    value: "dev"
  - key: "Owner"
    value: "DevSecOpsAdm"

Enter fullscreen mode Exit fullscreen mode

The main block is iam_properties, here the properties for IAM programmatic user are defined. First, the username, followed by context composed for start date and end date this time range indicate the valid range to assume principal role. Second, assume_roles parameters composed by ARN roles.

These conditions are good but could be better 👌

Those roles are creating for CDK when you run the cdk bootstrap command. Each role is created for a specific function and depends on the stack components one or more roles are used. The Figure 2 depicts the roles in an AWS Account.

cdk roles
Figure 2. CDK Roles

In the src/constructs/iam_role.py the IAMSetup construct is define:

from aws_cdk import (
    Aws,
    aws_iam as iam,
    aws_secretsmanager as secretsmanager,
    CfnOutput

)
from constructs import Construct


class IAMSetup(Construct):

    def __init__(self, scope: Construct, construct_id: str, props: dict, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)
        # Getting props
        project_name = props.get("project_name", "demo")
        roles = props.get("iam_properties", {}).get("assume_roles", [])
        user_name = props.get("iam_properties", {}).get("user_name", f"{project_name}_user")
        context = props.get("iam_properties", {}).get("context", {"from": "2023-01-01T00:00:00Z",
                                                                  "up": "2023-06-30T23:59:59Z"})

        rs = []
        if roles:
            for r in roles:
                rs.append(r["arn"])
        else:
            rs = [f"arn:aws:iam::{Aws.ACCOUNT_ID}:role/cdk-hnb659fds-cfn-exec-role-{Aws.ACCOUNT_ID}-{Aws.REGION}"]
        # Creating statement
        st = iam.PolicyStatement(actions=["sts:AssumeRole"], effect=iam.Effect.ALLOW, resources=rs)

        ext_id = secretsmanager.Secret(self, "IDSecret", secret_name=f"/{project_name}/SecretExternalID",
                                       generate_secret_string=secretsmanager.SecretStringGenerator(
                                           secret_string_template='{"type": "role_ext_id"}',
                                           exclude_punctuation=True,
                                           exclude_characters="-`",

                                           generate_string_key='external_id'
                                       )
                                       )
        self.role = iam.Role(self, "CustomRole", role_name=f"Role{project_name.capitalize()}",
                             assumed_by=iam.PrincipalWithConditions(iam.AccountPrincipal(account_id=Aws.ACCOUNT_ID),

                                                                    conditions={
                                                                        "StringLike": {
                                                                            "sts:RoleSessionName": "${aws:username}"
                                                                        },
                                                                        "DateGreaterThan": {
                                                                            "aws:CurrentTime": context["from"]},
                                                                        "DateLessThan": {
                                                                            "aws:CurrentTime": context["up"]}
                                                                    }
                                                                    ),

                             # Using a SecretValue here risks exposing your secret.
                             #  Call AWS Secrets Manager directly in your runtime code. Call 'secretValue.unsafeUnwrap()' if you understand and accept the risks.
                             external_ids=[ext_id.secret_value_from_json('external_id').unsafe_unwrap()]
                             )

        self.role.add_to_policy(st)
        self.role.node.add_dependency(ext_id)

        # Creating  iam programmatic user

        self.user = iam.User(self, user_name, user_name=user_name, path=f"/{project_name}/")
        # Create Access key and secret key for user
        access_key = iam.AccessKey(self, "AccessKey", user=self.user)

        secretsmanager.Secret(self, "Secret",
                              secret_name=f"/{project_name}/UserAccessKey",
                              secret_string_value=access_key.secret_access_key
                              )
        # Create Group
        self.group = iam.Group(self, f"Group_{project_name}", group_name=f"{project_name}Group")
        self.user.add_to_group(self.group)

        # Grant User assume role
        self.role.grant_assume_role(self.group)

        # Audit user Activity
        CfnOutput(self, "RoleARN", value=self.role.role_arn, description=f"Role ARN ")


Enter fullscreen mode Exit fullscreen mode

To improve the security and limit the context, the role session name condition, and external id is added.

self.role = iam.Role(self, "CustomRole", role_name=f"Role{project_name.capitalize()}",
                             assumed_by=iam.PrincipalWithConditions(iam.AccountPrincipal(account_id=Aws.ACCOUNT_ID),

                                                                    conditions={
                                                                        "StringLike": {
                                                                            "sts:RoleSessionName": "${aws:username}"
                                                                        },
                                                                        "DateGreaterThan": {
                                                                            "aws:CurrentTime": context["from"]},
                                                                        "DateLessThan": {
                                                                            "aws:CurrentTime": context["up"]}
                                                                    }
                                                                    ),

                             # Using a SecretValue here risks exposing your secret.
                             #  Call AWS Secrets Manager directly in your runtime code. Call 'secretValue.unsafeUnwrap()' if you understand and accept the risks.
                             external_ids=[ext_id.secret_value_from_json('external_id').unsafe_unwrap()]
                             )
... 
Enter fullscreen mode Exit fullscreen mode

After, the iam user is defined and his credentials are stored in secrets Manager.

...
self.user = iam.User(self, user_name, user_name=user_name, path=f"/{project_name}/")
        # Create Access key and secret key for user
        access_key = iam.AccessKey(self, "AccessKey", user=self.user)

        secretsmanager.Secret(self, "Secret",
                              secret_name=f"/{project_name}/UserAccessKey",
                              secret_string_value=access_key.secret_access_key
                              )
...
Enter fullscreen mode Exit fullscreen mode

Now, a group is created and grant permissions to asumme the custom role.

Remember, a good practice is attach policies to groups not to individual user.

# Create Group
        self.group = iam.Group(self, f"Group_{project_name}", group_name=f"{project_name}Group")
        self.user.add_to_group(self.group)

        # Grant User assume role
        self.role.grant_assume_role(self.group)


Enter fullscreen mode Exit fullscreen mode

Finally, your code is ready for deployment using the cdk deploy command.

cdk deploy --profile labxl-devsecops


✨  Synthesis time: 10.05s

Cdkv2ProgUserDeployStack: building assets...

[0%] start: Building 617ca871cad3076249f327a39b926d0ff6b6d69362952a1d35947fdd3851cea3:105171185823-us-east-2
[100%] success: Built 617ca871cad3076249f327a39b926d0ff6b6d69362952a1d35947fdd3851cea3:105171185823-us-east-2

Cdkv2ProgUserDeployStack: assets built

Cdkv2ProgUserDeployStack: deploying... [1/1]
[0%] start: Publishing 617ca871cad3076249f327a39b926d0ff6b6d69362952a1d35947fdd3851cea3:105171185823-us-east-2
[100%] success: Published 617ca871cad3076249f327a39b926d0ff6b6d69362952a1d35947fdd3851cea3:105171185823-us-east-2
Cdkv2ProgUserDeployStack: creating CloudFormation changeset...

 ✅  Cdkv2ProgUserDeployStack

✨  Deployment time: 91.12s


Enter fullscreen mode Exit fullscreen mode

You can explore the AWS console to check the resources.

Now, go to secrets manager and retrieve and view the secret value for the secret access key, and external id for assume role, then go to IAM console and get the role ARN or copy it from cdk deploy output command in the previous step.

Secrets Manager view
Figure 3. Secrets into the secrets manager

Azure DevOps and AWS Toolkit

Open azure DevOps portal and install AWS Toolkit for your organization.

Azure DevOps - Install AWS Toolkit
Figure 4. Install AWS Toolkit in Azure DevOps

Then create a service connection as is depicted in Figure 5.

Setup Service connection
Figure 5. Setup Service connection in Azure DevOps

The next step is creating a pipeline to use this service connection and deploying a test stack.

Thanks for reading and sharing! 😃

Top comments (0)