Skip to content

Commit bde1795

Browse files
authored
fix(elasticloadbalancingv2): ApplicationLoadBalancer.logAccessLogs does not grant all necessary permissions (#18558)
`ALB.logAccessLogs` today grants the ELB account Put/Abort on the bucket. The NLB code extends this to also grant permissions to the `delivery.logs.amazonaws.com` service principal. The ALB documentation now states that the permissions required for ALB are the same as NLB, so consolidating the code back into the base. fixes #18367 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent d71b931 commit bde1795

File tree

3 files changed

+207
-120
lines changed

3 files changed

+207
-120
lines changed

packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
22
import * as ec2 from '@aws-cdk/aws-ec2';
3-
import { PolicyStatement, ServicePrincipal } from '@aws-cdk/aws-iam';
4-
import { IBucket } from '@aws-cdk/aws-s3';
53
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
64
import { Resource } from '@aws-cdk/core';
75
import * as cxapi from '@aws-cdk/cx-api';
@@ -125,41 +123,6 @@ export class NetworkLoadBalancer extends BaseLoadBalancer implements INetworkLoa
125123
});
126124
}
127125

128-
/**
129-
* Enable access logging for this load balancer.
130-
*
131-
* A region must be specified on the stack containing the load balancer; you cannot enable logging on
132-
* environment-agnostic stacks. See https://docs.aws.amazon.com/cdk/latest/guide/environments.html
133-
*
134-
* This is extending the BaseLoadBalancer.logAccessLogs method to match the bucket permissions described
135-
* at https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-access-logs.html#access-logging-bucket-requirements
136-
*/
137-
public logAccessLogs(bucket: IBucket, prefix?: string) {
138-
super.logAccessLogs(bucket, prefix);
139-
140-
const logsDeliveryServicePrincipal = new ServicePrincipal('delivery.logs.amazonaws.com');
141-
142-
bucket.addToResourcePolicy(
143-
new PolicyStatement({
144-
actions: ['s3:PutObject'],
145-
principals: [logsDeliveryServicePrincipal],
146-
resources: [
147-
bucket.arnForObjects(`${prefix ? prefix + '/' : ''}AWSLogs/${this.stack.account}/*`),
148-
],
149-
conditions: {
150-
StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' },
151-
},
152-
}),
153-
);
154-
bucket.addToResourcePolicy(
155-
new PolicyStatement({
156-
actions: ['s3:GetBucketAcl'],
157-
principals: [logsDeliveryServicePrincipal],
158-
resources: [bucket.bucketArn],
159-
}),
160-
);
161-
}
162-
163126
/**
164127
* Return the given named metric for this Network Load Balancer
165128
*
@@ -326,4 +289,4 @@ class LookedUpNetworkLoadBalancer extends Resource implements INetworkLoadBalanc
326289
...props,
327290
});
328291
}
329-
}
292+
}

packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as ec2 from '@aws-cdk/aws-ec2';
22
import * as iam from '@aws-cdk/aws-iam';
3+
import { PolicyStatement, ServicePrincipal } from '@aws-cdk/aws-iam';
34
import * as s3 from '@aws-cdk/aws-s3';
45
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
56
import { ContextProvider, IResource, Lazy, Resource, Stack, Token } from '@aws-cdk/core';
@@ -258,7 +259,27 @@ export abstract class BaseLoadBalancer extends Resource {
258259
throw new Error(`Cannot enable access logging; don't know ELBv2 account for region ${region}`);
259260
}
260261

262+
const logsDeliveryServicePrincipal = new ServicePrincipal('delivery.logs.amazonaws.com');
261263
bucket.grantPut(new iam.AccountPrincipal(account), `${(prefix ? prefix + '/' : '')}AWSLogs/${Stack.of(this).account}/*`);
264+
bucket.addToResourcePolicy(
265+
new PolicyStatement({
266+
actions: ['s3:PutObject'],
267+
principals: [logsDeliveryServicePrincipal],
268+
resources: [
269+
bucket.arnForObjects(`${prefix ? prefix + '/' : ''}AWSLogs/${this.stack.account}/*`),
270+
],
271+
conditions: {
272+
StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' },
273+
},
274+
}),
275+
);
276+
bucket.addToResourcePolicy(
277+
new PolicyStatement({
278+
actions: ['s3:GetBucketAcl'],
279+
principals: [logsDeliveryServicePrincipal],
280+
resources: [bucket.bucketArn],
281+
}),
282+
);
262283

263284
// make sure the bucket's policy is created before the ALB (see https://github.com/aws/aws-cdk/issues/1633)
264285
this.node.addDependency(bucket);

packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts

Lines changed: 185 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Match, Template } from '@aws-cdk/assertions';
22
import { Metric } from '@aws-cdk/aws-cloudwatch';
33
import * as ec2 from '@aws-cdk/aws-ec2';
44
import * as s3 from '@aws-cdk/aws-s3';
5-
import { testFutureBehavior } from '@aws-cdk/cdk-build-tools/lib/feature-flag';
5+
import { testFutureBehavior, testLegacyBehavior } from '@aws-cdk/cdk-build-tools/lib/feature-flag';
66
import * as cdk from '@aws-cdk/core';
77
import * as cxapi from '@aws-cdk/cx-api';
88
import * as elbv2 from '../../lib';
@@ -146,105 +146,208 @@ describe('tests', () => {
146146
expect(loadBalancer.listeners).toContain(listener);
147147
});
148148

149-
testFutureBehavior('Access logging', s3GrantWriteCtx, cdk.App, (app) => {
150-
// GIVEN
151-
const stack = new cdk.Stack(app, undefined, { env: { region: 'us-east-1' } });
152-
const vpc = new ec2.Vpc(stack, 'Stack');
153-
const bucket = new s3.Bucket(stack, 'AccessLoggingBucket');
154-
const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc });
149+
describe('logAccessLogs', () => {
155150

156-
// WHEN
157-
lb.logAccessLogs(bucket);
151+
function loggingSetup(app: cdk.App): { stack: cdk.Stack, bucket: s3.Bucket, lb: elbv2.ApplicationLoadBalancer } {
152+
const stack = new cdk.Stack(app, undefined, { env: { region: 'us-east-1' } });
153+
const vpc = new ec2.Vpc(stack, 'Stack');
154+
const bucket = new s3.Bucket(stack, 'AccessLoggingBucket');
155+
const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc });
156+
return { stack, bucket, lb };
157+
}
158158

159-
// THEN
159+
test('sets load balancer attributes', () => {
160+
// GIVEN
161+
const app = new cdk.App();
162+
const { stack, bucket, lb } = loggingSetup(app);
160163

161-
// verify that the LB attributes reference the bucket
162-
Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', {
163-
LoadBalancerAttributes: Match.arrayWith([
164-
{
165-
Key: 'access_logs.s3.enabled',
166-
Value: 'true',
167-
},
168-
{
169-
Key: 'access_logs.s3.bucket',
170-
Value: { Ref: 'AccessLoggingBucketA6D88F29' },
171-
},
172-
{
173-
Key: 'access_logs.s3.prefix',
174-
Value: '',
175-
},
176-
]),
177-
});
164+
// WHEN
165+
lb.logAccessLogs(bucket);
178166

179-
// verify the bucket policy allows the ALB to put objects in the bucket
180-
Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', {
181-
PolicyDocument: {
182-
Version: '2012-10-17',
183-
Statement: [
167+
//THEN
168+
Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', {
169+
LoadBalancerAttributes: Match.arrayWith([
184170
{
185-
Action: ['s3:PutObject', 's3:Abort*'],
186-
Effect: 'Allow',
187-
Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } },
188-
Resource: {
189-
'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/AWSLogs/',
190-
{ Ref: 'AWS::AccountId' }, '/*']],
191-
},
171+
Key: 'access_logs.s3.enabled',
172+
Value: 'true',
192173
},
193-
],
194-
},
174+
{
175+
Key: 'access_logs.s3.bucket',
176+
Value: { Ref: 'AccessLoggingBucketA6D88F29' },
177+
},
178+
{
179+
Key: 'access_logs.s3.prefix',
180+
Value: '',
181+
},
182+
]),
183+
});
195184
});
196185

197-
// verify the ALB depends on the bucket *and* the bucket policy
198-
Template.fromStack(stack).hasResource('AWS::ElasticLoadBalancingV2::LoadBalancer', {
199-
DependsOn: ['AccessLoggingBucketPolicy700D7CC6', 'AccessLoggingBucketA6D88F29'],
186+
test('adds a dependency on the bucket', () => {
187+
// GIVEN
188+
const app = new cdk.App();
189+
const { stack, bucket, lb } = loggingSetup(app);
190+
191+
// WHEN
192+
lb.logAccessLogs(bucket);
193+
194+
// THEN
195+
// verify the ALB depends on the bucket *and* the bucket policy
196+
Template.fromStack(stack).hasResource('AWS::ElasticLoadBalancingV2::LoadBalancer', {
197+
DependsOn: ['AccessLoggingBucketPolicy700D7CC6', 'AccessLoggingBucketA6D88F29'],
198+
});
200199
});
201-
});
202200

203-
testFutureBehavior('access logging with prefix', s3GrantWriteCtx, cdk.App, (app) => {
204-
// GIVEN
205-
const stack = new cdk.Stack(app, undefined, { env: { region: 'us-east-1' } });
206-
const vpc = new ec2.Vpc(stack, 'Stack');
207-
const bucket = new s3.Bucket(stack, 'AccessLoggingBucket');
208-
const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc });
201+
testLegacyBehavior('legacy bucket permissions', cdk.App, (app) => {
202+
const { stack, bucket, lb } = loggingSetup(app);
209203

210-
// WHEN
211-
lb.logAccessLogs(bucket, 'prefix-of-access-logs');
204+
// WHEN
205+
lb.logAccessLogs(bucket);
212206

213-
// THEN
214-
// verify that the LB attributes reference the bucket
215-
Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', {
216-
LoadBalancerAttributes: Match.arrayWith([
217-
{
218-
Key: 'access_logs.s3.enabled',
219-
Value: 'true',
220-
},
221-
{
222-
Key: 'access_logs.s3.bucket',
223-
Value: { Ref: 'AccessLoggingBucketA6D88F29' },
207+
// THEN
208+
// verify the bucket policy allows the ALB to put objects in the bucket
209+
Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', {
210+
PolicyDocument: {
211+
Version: '2012-10-17',
212+
Statement: [
213+
{
214+
Action: ['s3:PutObject*', 's3:Abort*'],
215+
Effect: 'Allow',
216+
Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } },
217+
Resource: {
218+
'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/AWSLogs/',
219+
{ Ref: 'AWS::AccountId' }, '/*']],
220+
},
221+
},
222+
{
223+
Action: 's3:PutObject',
224+
Effect: 'Allow',
225+
Principal: { Service: 'delivery.logs.amazonaws.com' },
226+
Resource: {
227+
'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/AWSLogs/',
228+
{ Ref: 'AWS::AccountId' }, '/*']],
229+
},
230+
Condition: { StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' } },
231+
},
232+
{
233+
Action: 's3:GetBucketAcl',
234+
Effect: 'Allow',
235+
Principal: { Service: 'delivery.logs.amazonaws.com' },
236+
Resource: {
237+
'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'],
238+
},
239+
},
240+
],
224241
},
225-
{
226-
Key: 'access_logs.s3.prefix',
227-
Value: 'prefix-of-access-logs',
242+
});
243+
});
244+
245+
testFutureBehavior('logging bucket permissions', s3GrantWriteCtx, cdk.App, (app) => {
246+
// GIVEN
247+
const { stack, bucket, lb } = loggingSetup(app);
248+
249+
// WHEN
250+
lb.logAccessLogs(bucket);
251+
252+
// THEN
253+
// verify the bucket policy allows the ALB to put objects in the bucket
254+
Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', {
255+
PolicyDocument: {
256+
Version: '2012-10-17',
257+
Statement: [
258+
{
259+
Action: ['s3:PutObject', 's3:Abort*'],
260+
Effect: 'Allow',
261+
Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } },
262+
Resource: {
263+
'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/AWSLogs/',
264+
{ Ref: 'AWS::AccountId' }, '/*']],
265+
},
266+
},
267+
{
268+
Action: 's3:PutObject',
269+
Effect: 'Allow',
270+
Principal: { Service: 'delivery.logs.amazonaws.com' },
271+
Resource: {
272+
'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/AWSLogs/',
273+
{ Ref: 'AWS::AccountId' }, '/*']],
274+
},
275+
Condition: { StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' } },
276+
},
277+
{
278+
Action: 's3:GetBucketAcl',
279+
Effect: 'Allow',
280+
Principal: { Service: 'delivery.logs.amazonaws.com' },
281+
Resource: {
282+
'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'],
283+
},
284+
},
285+
],
228286
},
229-
]),
287+
});
230288
});
231289

232-
// verify the bucket policy allows the ALB to put objects in the bucket
233-
Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', {
234-
PolicyDocument: {
235-
Version: '2012-10-17',
236-
Statement: [
290+
testFutureBehavior('access logging with prefix', s3GrantWriteCtx, cdk.App, (app) => {
291+
// GIVEN
292+
const { stack, bucket, lb } = loggingSetup(app);
293+
294+
// WHEN
295+
lb.logAccessLogs(bucket, 'prefix-of-access-logs');
296+
297+
// THEN
298+
// verify that the LB attributes reference the bucket
299+
Template.fromStack(stack).hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', {
300+
LoadBalancerAttributes: Match.arrayWith([
237301
{
238-
Action: ['s3:PutObject', 's3:Abort*'],
239-
Effect: 'Allow',
240-
Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } },
241-
Resource: {
242-
'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/prefix-of-access-logs/AWSLogs/',
243-
{ Ref: 'AWS::AccountId' }, '/*']],
244-
},
302+
Key: 'access_logs.s3.enabled',
303+
Value: 'true',
304+
},
305+
{
306+
Key: 'access_logs.s3.bucket',
307+
Value: { Ref: 'AccessLoggingBucketA6D88F29' },
308+
},
309+
{
310+
Key: 'access_logs.s3.prefix',
311+
Value: 'prefix-of-access-logs',
245312
},
246-
],
247-
},
313+
]),
314+
});
315+
316+
// verify the bucket policy allows the ALB to put objects in the bucket
317+
Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', {
318+
PolicyDocument: {
319+
Version: '2012-10-17',
320+
Statement: [
321+
{
322+
Action: ['s3:PutObject', 's3:Abort*'],
323+
Effect: 'Allow',
324+
Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } },
325+
Resource: {
326+
'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/prefix-of-access-logs/AWSLogs/',
327+
{ Ref: 'AWS::AccountId' }, '/*']],
328+
},
329+
},
330+
{
331+
Action: 's3:PutObject',
332+
Effect: 'Allow',
333+
Principal: { Service: 'delivery.logs.amazonaws.com' },
334+
Resource: {
335+
'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/prefix-of-access-logs/AWSLogs/',
336+
{ Ref: 'AWS::AccountId' }, '/*']],
337+
},
338+
Condition: { StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' } },
339+
},
340+
{
341+
Action: 's3:GetBucketAcl',
342+
Effect: 'Allow',
343+
Principal: { Service: 'delivery.logs.amazonaws.com' },
344+
Resource: {
345+
'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'],
346+
},
347+
},
348+
],
349+
},
350+
});
248351
});
249352
});
250353

0 commit comments

Comments
 (0)