diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index dcc904c..691be18 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -13,10 +13,38 @@ jobs: - name: Set up Python 3 uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' - name: Install dependencies run: | pip install -r requirements.txt - name: Run Pytest run: | python -m pytest -v + cdk_nag: + runs-on: ubuntu-latest + defaults: + run: + working-directory: cdk + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3 + uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Install dependencies + run: | + npm install -g aws-cdk + pip install -r requirements.txt + - name: Setup test environment + run: | + export CDK_DEFAULT_ACCOUNT=012345678912 + export CDK_DEFAULT_REGION=us-west-2 + cp tests/cdk.context.json . + - name: Synth pipeline and jenkins server stacks + run: | + cdk synth \ + --context codestar-connection=arn:aws:codestar-connections/connection_id \ + --context repo=org/repo \ + --context branch=branch \ + --context cert-arn=arn:aws:acm:certificate/certificate_id + cdk synth --app "python jenkins_server/app.py" --context cert-arn=arn:aws:acm:certificate/certificate_id --no-lookups diff --git a/cdk/app.py b/cdk/app.py index c272378..0db28ef 100644 --- a/cdk/app.py +++ b/cdk/app.py @@ -10,6 +10,7 @@ import sys import aws_cdk as cdk +from cdk_nag import AwsSolutionsChecks from pipeline import JenkinsPipeline, MissingContextError # Account and region set by the default AWS profile or one specified with --profile @@ -17,6 +18,7 @@ REGION = os.environ.get('CDK_DEFAULT_REGION') app = cdk.App() +cdk.Aspects.of(app).add(AwsSolutionsChecks(verbose=True)) try: JenkinsPipeline(app, 'JenkinsPipelineStack', env=cdk.Environment(account=ACCOUNT, region=REGION)) diff --git a/cdk/jenkins_server/app.py b/cdk/jenkins_server/app.py index 470db6a..c9b1e2e 100644 --- a/cdk/jenkins_server/app.py +++ b/cdk/jenkins_server/app.py @@ -10,6 +10,7 @@ import sys import aws_cdk as cdk +from cdk_nag import AwsSolutionsChecks from jenkins_server import JenkinsServerStack # Account and region set by the default AWS profile or one specified with --profile @@ -17,6 +18,7 @@ REGION = os.environ.get('CDK_DEFAULT_REGION') app = cdk.App() +cdk.Aspects.of(app).add(AwsSolutionsChecks(verbose=True)) try: JenkinsServerStack(app, 'JenkinsServerStack', diff --git a/cdk/jenkins_server/jenkins_server.py b/cdk/jenkins_server/jenkins_server.py index 5a17138..48f3488 100644 --- a/cdk/jenkins_server/jenkins_server.py +++ b/cdk/jenkins_server/jenkins_server.py @@ -13,12 +13,14 @@ import aws_cdk.aws_efs as efs import aws_cdk.aws_elasticloadbalancingv2 as elb import aws_cdk.aws_iam as iam +import aws_cdk.aws_kms as kms import aws_cdk.aws_logs as logs import aws_cdk.aws_sns as sns import aws_cdk.aws_s3 as s3 from os import path from aws_cdk import Stack +from cdk_nag import NagSuppressions from constructs import Construct @@ -43,11 +45,13 @@ def __init__(self, scope: Construct, id: str, **kwargs) -> None: self.cert_arn = self._load_cert_arn() self.vpc = self._create_vpc() - self.build_topic = sns.Topic(self, 'BuildTopic') + self.build_topic = sns.Topic(self, 'BuildTopic', master_key=kms.Key(self, "SNSKey", enable_key_rotation=True)) self.log_group = logs.LogGroup(self, 'LogGroup') self.file_system, self.access_point = self._create_efs() self.fargate_service = self._create_ecs() self._create_alb() + self._add_nag_suppressions() + def _load_stack_config(self, config_file): """Load stack config. The config file is expected to be in the same directory.""" @@ -69,12 +73,14 @@ def _load_cert_arn(self): def _create_vpc(self): """Create a new VPC or use an existing one if a VPC ID is provided.""" - if self.stack_tags['vpc-id'] == 'None': # Context values will be converted to string and cannot be empty during synth - return ec2.Vpc(self, 'VPC', + if self.stack_tags.get('vpc-id', 'None') == 'None': # Tag values from pipeline will be converted to string and cannot be empty during synth + vpc = ec2.Vpc(self, 'VPC', cidr=self.stack_config['vpc']['cidr'], nat_gateways=self.stack_config['vpc']['nat_gateways'] ) - return ec2.Vpc.from_lookup(self, 'VPC', vpc_id=self.stack_tags['vpc-id']) + vpc.add_flow_log("JenkinsVPCFlowLog") + return vpc + return ec2.Vpc.from_lookup(self, 'VPC', vpc_id=self.stack_tags.get('vpc-id')) def _create_efs(self): """Create a file system with an access point for the jenkins home directory.""" @@ -216,7 +222,8 @@ def _create_alb(self): alb.log_access_logs( s3.Bucket(self, 'AccessLogsBucket', block_public_access=s3.BlockPublicAccess.BLOCK_ALL, - encryption=s3.BucketEncryption.S3_MANAGED + encryption=s3.BucketEncryption.S3_MANAGED, + enforce_ssl=True ) ) @@ -225,3 +232,21 @@ def _create_alb(self): if alb_config['public'] is True: alb.connections.allow_from_any_ipv4(ec2.Port.tcp(443)) + + def _add_nag_suppressions(self): + '''Add cdk-nag suppressions for the Jenkins server stack.''' + suppression_list = [ + { + 'id': 'AwsSolutions-IAM5', + 'reason': 'Wildcard permissions required to allow pulling user added Jenkins configs from parameter store.' + }, + { + 'id': 'AwsSolutions-EC23', + 'reason': 'Allows enabling public access to server. Additional IP based security will be setup through WAF.' + }, + { + 'id': 'AwsSolutions-S1', + 'reason': 'Access logs bucket does not need access logging enabled.' + } + ] + NagSuppressions.add_stack_suppressions(self, suppression_list) diff --git a/cdk/pipeline.py b/cdk/pipeline.py index 0fcab0d..6281b60 100644 --- a/cdk/pipeline.py +++ b/cdk/pipeline.py @@ -11,6 +11,7 @@ import aws_cdk.pipelines as pipelines from aws_cdk import Environment, Stack, Stage +from cdk_nag import NagSuppressions from constructs import Construct from jenkins_server.jenkins_server import JenkinsServerStack @@ -50,6 +51,7 @@ def __init__(self, scope: Construct, id: str, **kwargs) -> None: self.source = pipelines.CodePipelineSource.connection(self.repo, self.branch, connection_arn=self.codestar_connection) self._create_pipeline() + self._add_nag_suppressions() def _get_required_context(self, context_name): """Get context value and raise an exception if it does not exist.""" @@ -119,3 +121,30 @@ def _create_pipeline(self): pipelines.ManualApprovalStep('ReleaseToProd') ] ) + + def _add_nag_suppressions(self): + '''Add cdk-nag suppressions for the pipeline stack. + + The CDK Pipeline library generates internal constructs that are not defined here but may be generate rule violations + for cdk-nag. Adding suppressions at stack level to avoid errors. + + ''' + suppression_list = [ + { + 'id': 'AwsSolutions-S1', + 'reason': 'CDK Pipeline generates its own S3 buckets for pipeline assets.' + }, + { + 'id': 'AwsSolutions-IAM5', + 'reason': 'CDK Pipeline generates its own IAM permissions.' + }, + { + 'id': 'AwsSolutions-CB3', + 'reason': 'CDK Pipeline requires privileged mode for its CodeBuild project to build docker images.' + }, + { + 'id': 'AwsSolutions-CB4', + 'reason': 'CDK Pipeline generates its own CodeBuild project for pipeline assets.' + } + ] + NagSuppressions.add_stack_suppressions(self, suppression_list) diff --git a/cdk/requirements.txt b/cdk/requirements.txt index 7c3d4e2..0b0fb55 100644 --- a/cdk/requirements.txt +++ b/cdk/requirements.txt @@ -1,18 +1,21 @@ -attrs==22.1.0 -aws-cdk-lib==2.95.1 -cattrs==22.1.0 -constructs==10.1.84 -exceptiongroup==1.0.0rc8 -iniconfig==1.1.1 -jsii==1.88.0 -packaging==21.3 -pluggy==1.0.0 +attrs==23.1.0 +aws-cdk-lib==2.103.1 +aws-cdk.asset-awscli-v1==2.2.201 +aws-cdk.asset-kubectl-v20==2.1.2 +aws-cdk.asset-node-proxy-agent-v6==2.0.1 +cattrs==23.1.2 +cdk-nag==2.27.179 +constructs==10.3.0 +exceptiongroup==1.1.3 +importlib-resources==6.1.0 +iniconfig==2.0.0 +jsii==1.91.0 +packaging==23.2 +pluggy==1.3.0 publication==0.0.3 -py==1.11.0 -pyparsing==3.0.9 -pytest==7.1.2 +pytest==7.4.3 python-dateutil==2.8.2 six==1.16.0 tomli==2.0.1 typeguard==2.13.3 -typing_extensions==4.3.0 +typing_extensions==4.8.0 diff --git a/cdk/tests/cdk.context.json b/cdk/tests/cdk.context.json new file mode 100644 index 0000000..8859618 --- /dev/null +++ b/cdk/tests/cdk.context.json @@ -0,0 +1,8 @@ +{ + "availability-zones:account=012345678912:region=us-west-2": [ + "us-west-2a", + "us-west-2b", + "us-west-2c", + "us-west-2d" + ] + } diff --git a/cdk/tests/test_jenkins_server_stack.py b/cdk/tests/test_jenkins_server_stack.py index c32d983..9f248da 100644 --- a/cdk/tests/test_jenkins_server_stack.py +++ b/cdk/tests/test_jenkins_server_stack.py @@ -47,7 +47,7 @@ def test_stack_context_values(template): def test_required_resources(template): template.resource_count_is("AWS::EC2::VPC", 1) template.resource_count_is("AWS::SNS::Topic", 1) - template.resource_count_is("AWS::Logs::LogGroup", 1) + template.resource_count_is("AWS::Logs::LogGroup", 2) template.resource_count_is("AWS::EFS::FileSystem", 1) template.resource_count_is("AWS::EFS::AccessPoint", 1) template.resource_count_is("AWS::ECS::Cluster", 1)