From 30cb8a3b95f9f0b1a7287a23e9b4f446bb403f51 Mon Sep 17 00:00:00 2001 From: Sayali Gaikawad <61760125+gaiksaya@users.noreply.github.com> Date: Tue, 19 Mar 2024 16:10:48 -0700 Subject: [PATCH] Enable access logging for load balancer (#401) Signed-off-by: Sayali Gaikawad --- .gitignore | 2 +- lib/ci-stack.ts | 1 + lib/network/ci-external-load-balancer.ts | 26 ++++++- test/ci-stack.test.ts | 91 ++++++++++++++++++++++-- test/compute/agent-node-config.test.ts | 54 ++------------ 5 files changed, 116 insertions(+), 58 deletions(-) diff --git a/.gitignore b/.gitignore index fa0f3719..0b89b8cf 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,7 @@ resources/jenkins.yaml # CDK asset staging directory .cdk.staging cdk.out - +cdk.context.json # excluding intellij Idea files *.iml .idea/ diff --git a/lib/ci-stack.ts b/lib/ci-stack.ts index be53b17b..c906ce9e 100644 --- a/lib/ci-stack.ts +++ b/lib/ci-stack.ts @@ -199,6 +199,7 @@ export class CIStack extends Stack { targetInstance: mainJenkinsNode.mainNodeAsg, listenerCertificate, useSsl, + accessLogBucket: auditloggingS3Bucket.bucket, }); const artifactBucket = new Bucket(this, 'BuildBucket'); diff --git a/lib/network/ci-external-load-balancer.ts b/lib/network/ci-external-load-balancer.ts index cfbafa29..4d951c71 100644 --- a/lib/network/ci-external-load-balancer.ts +++ b/lib/network/ci-external-load-balancer.ts @@ -7,12 +7,13 @@ */ import { CfnOutput, Stack } from 'aws-cdk-lib'; -import { Instance, SecurityGroup, Vpc } from 'aws-cdk-lib/aws-ec2'; +import { AutoScalingGroup } from 'aws-cdk-lib/aws-autoscaling'; +import { SecurityGroup, Vpc } from 'aws-cdk-lib/aws-ec2'; import { ApplicationListener, ApplicationLoadBalancer, ApplicationProtocol, ApplicationTargetGroup, ListenerCertificate, Protocol, SslPolicy, } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; -import { InstanceTarget } from 'aws-cdk-lib/aws-elasticloadbalancingv2-targets'; -import { AutoScalingGroup } from 'aws-cdk-lib/aws-autoscaling'; +import { PolicyStatement, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; +import { Bucket, BucketPolicy } from 'aws-cdk-lib/aws-s3'; export interface JenkinsExternalLoadBalancerProps { readonly vpc: Vpc; @@ -20,6 +21,7 @@ export interface JenkinsExternalLoadBalancerProps { readonly targetInstance: AutoScalingGroup; readonly listenerCertificate: ListenerCertificate; readonly useSsl: boolean; + readonly accessLogBucket: Bucket; } export class JenkinsExternalLoadBalancer { @@ -31,6 +33,7 @@ export class JenkinsExternalLoadBalancer { constructor(stack: Stack, props: JenkinsExternalLoadBalancerProps) { const accessPort = props.useSsl ? 443 : 80; + const accessLoggingPrefix = 'loadBalancerAccessLogs'; // Using an ALB so it can be part of a security group rather than by whitelisting ip addresses this.loadBalancer = new ApplicationLoadBalancer(stack, 'JenkinsALB', { @@ -64,6 +67,23 @@ export class JenkinsExternalLoadBalancer { }, }); + this.loadBalancer.logAccessLogs(props.accessLogBucket, accessLoggingPrefix); + + const bucketPolicy = new BucketPolicy(stack, 'ALBaccessLoggingBucketPolicyPErmission', { + bucket: props.accessLogBucket, + }); + + bucketPolicy.document.addStatements( + new PolicyStatement({ + actions: ['s3:PutObject'], + principals: [ + new ServicePrincipal('logdelivery.elasticloadbalancing.amazonaws.com'), + ], + resources: [`${props.accessLogBucket.bucketArn}/${accessLoggingPrefix}/*`], + + }), + ); + new CfnOutput(stack, 'Jenkins External Load Balancer Dns', { value: this.loadBalancer.loadBalancerDnsName, }); diff --git a/test/ci-stack.test.ts b/test/ci-stack.test.ts index 672d6760..ab1961e0 100644 --- a/test/ci-stack.test.ts +++ b/test/ci-stack.test.ts @@ -21,6 +21,7 @@ test('CI Stack Basic Resources', () => { // WHEN const stack = new CIStack(app, 'TestStack', { dataRetention: true, + env: { account: 'test-account', region: 'us-east-1' }, }); const template = Template.fromStack(stack); @@ -49,7 +50,7 @@ test('External security group is open', () => { }); // WHEN - const stack = new CIStack(app, 'MyTestStack', {}); + const stack = new CIStack(app, 'MyTestStack', { env: { account: 'test-account', region: 'us-east-1' } }); const template = Template.fromStack(stack); // THEN @@ -93,7 +94,11 @@ test('External security group is restricted', () => { }); // WHEN - const stack = new CIStack(app, 'MyTestStack', { useSsl: true, restrictServerAccessTo: Peer.ipv4('10.0.0.0/24') }); + const stack = new CIStack(app, 'MyTestStack', { + env: { account: 'test-account', region: 'us-east-1' }, + useSsl: true, + restrictServerAccessTo: Peer.ipv4('10.0.0.0/24'), + }); const template = Template.fromStack(stack); // THEN @@ -135,7 +140,9 @@ test('MainNode', () => { }); // WHEN - const stack = new CIStack(app, 'MyTestStack', {}); + const stack = new CIStack(app, 'MyTestStack', { + env: { account: 'test-account', region: 'us-east-1' }, + }); // THEN Template.fromStack(stack).hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { @@ -165,7 +172,9 @@ test('LoadBalancer', () => { }); // WHEN - const stack = new CIStack(app, 'MyTestStack', {}); + const stack = new CIStack(app, 'MyTestStack', { + env: { account: 'test-account', region: 'us-east-1' }, + }); // THEN Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { @@ -188,7 +197,9 @@ test('CloudwatchCpuAlarm', () => { }); // WHEN - const stack = new CIStack(app, 'MyTestStack', {}); + const stack = new CIStack(app, 'MyTestStack', { + env: { account: 'test-account', region: 'us-east-1' }, + }); // THEN Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { @@ -205,7 +216,9 @@ test('CloudwatchMemoryAlarm', () => { }); // WHEN - const stack = new CIStack(app, 'MyTestStack', {}); + const stack = new CIStack(app, 'MyTestStack', { + env: { account: 'test-account', region: 'us-east-1' }, + }); // THEN Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { @@ -213,3 +226,69 @@ test('CloudwatchMemoryAlarm', () => { Statistic: 'Average', }); }); + +test('LoadBalancer Access Logging', () => { + const app = new App({ + context: { + useSsl: 'false', runWithOidc: 'false', serverAccessType: 'ipv4', restrictServerAccessTo: '10.10.10.10/32', + }, + }); + + // WHEN + const stack = new CIStack(app, 'MyTestStack', { + env: { account: 'test-account', region: 'us-east-1' }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', { + LoadBalancerAttributes: [ + { + Key: 'deletion_protection.enabled', + Value: 'false', + }, + { + Key: 'access_logs.s3.enabled', + Value: 'true', + }, + { + Key: 'access_logs.s3.bucket', + Value: { + Ref: 'jenkinsAuditBucket110D3080', + }, + }, + { + Key: 'access_logs.s3.prefix', + Value: 'loadBalancerAccessLogs', + }, + ], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', { + PolicyDocument: { + Statement: [ + { + Action: 's3:PutObject', + Effect: 'Allow', + Principal: { + Service: 'logdelivery.elasticloadbalancing.amazonaws.com', + }, + Resource: { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'jenkinsAuditBucket110D3080', + 'Arn', + ], + }, + '/loadBalancerAccessLogs/*', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + }); +}); diff --git a/test/compute/agent-node-config.test.ts b/test/compute/agent-node-config.test.ts index 1e942eee..803ac7ff 100644 --- a/test/compute/agent-node-config.test.ts +++ b/test/compute/agent-node-config.test.ts @@ -18,7 +18,9 @@ test('Agents Resource is present', () => { useSsl: 'true', runWithOidc: 'true', serverAccessType: 'ipv4', restrictServerAccessTo: '10.10.10.10/32', }, }); - const stack = new CIStack(app, 'TestStack', {}); + const stack = new CIStack(app, 'TestStack', { + env: { account: 'test-account', region: 'us-east-1' }, + }); const template = Template.fromStack(stack); template.hasResourceProperties('AWS::IAM::Role', { @@ -29,17 +31,7 @@ test('Agents Resource is present', () => { Action: 'sts:AssumeRole', Effect: 'Allow', Principal: { - Service: { - 'Fn::Join': [ - '', - [ - 'ec2.', - { - Ref: 'AWS::URLSuffix', - }, - ], - ], - }, + Service: 'ec2.amazonaws.com', }, }, ], @@ -71,49 +63,14 @@ test('Agents Resource is present', () => { 'ecr-public:CompleteLayerUpload', 'ecr-public:PutImage', ], - Condition: { - StringEquals: { - 'aws:RequestedRegion': { - Ref: 'AWS::Region', - }, - 'aws:PrincipalAccount': [ - { - Ref: 'AWS::AccountId', - }, - ], - }, - }, Effect: 'Allow', - Resource: { - 'Fn::Join': [ - '', - [ - 'arn:aws:ecr-public::', - { - Ref: 'AWS::AccountId', - }, - ':repository/*', - ], - ], - }, + Resource: 'arn:aws:ecr-public::test-account:repository/*', }, { Action: [ 'ecr-public:GetAuthorizationToken', 'sts:GetServiceBearerToken', ], - Condition: { - StringEquals: { - 'aws:RequestedRegion': { - Ref: 'AWS::Region', - }, - 'aws:PrincipalAccount': [ - { - Ref: 'AWS::AccountId', - }, - ], - }, - }, Effect: 'Allow', Resource: '*', }, @@ -131,6 +88,7 @@ test('Agents Node policy with assume role Resource is present', () => { }); const stack = new CIStack(app, 'TestStack', { agentAssumeRole: ['arn:aws:iam::12345:role/test-role', 'arn:aws:iam::901523:role/test-role2'], + env: { account: 'test-account', region: 'us-east-1' }, }); Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: {