Skip to content

Commit 9e34d5c

Browse files
Added metadata collection option (#976)
* Added metadata collection option * Added read-only roles and fixed file copy * added ASEA metadata collection * fixed descriptions * fixed prettier problem * fixed deletion problem with keys * Fixed bug with iam policies * fixed logic error * Changed bucket to log account * fixed acl for metadata objects * fixed acl for copy objects * Added out of box managed pol for metadata role * fixed ts error Co-authored-by: hickeydh-aws <hickeydh@amazon.com> Co-authored-by: Brian969 <56414362+Brian969@users.noreply.github.com>
1 parent e7c7112 commit 9e34d5c

File tree

12 files changed

+521
-4
lines changed

12 files changed

+521
-4
lines changed

src/deployments/cdk/src/apps/phase-2.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import * as snsDeployment from '../deployments/sns';
4141
import * as ssmDeployment from '../deployments/ssm';
4242
import { getStackJsonOutput } from '@aws-accelerator/common-outputs/src/stack-output';
4343
import { logArchiveReadOnlyAccess } from '../deployments/s3/log-archive-read-access';
44+
import * as metadataDeployment from '../deployments/metadata-collection';
4445
import { IamRoleOutputFinder } from '@aws-accelerator/common-outputs/src/iam-role';
4546
import * as alb from '../deployments/alb';
4647

@@ -453,6 +454,17 @@ export async function deploy({
453454
acceleratorPrefix: context.acceleratorPrefix,
454455
});
455456

457+
if (acceleratorConfig['global-options']['meta-data-collection']) {
458+
metadataDeployment.createMetadataService({
459+
acceleratorPrefix: context.acceleratorPrefix,
460+
accountStacks,
461+
accounts,
462+
config: acceleratorConfig,
463+
configBucket: centralBucket.bucketName,
464+
outputs,
465+
});
466+
}
467+
456468
await tgwDeployment.acceptPeeringAttachment({
457469
accountStacks,
458470
accounts,

src/deployments/cdk/src/deployments/defaults/step-1.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
import * as cdk from '@aws-cdk/core';
1515
import * as iam from '@aws-cdk/aws-iam';
1616
import * as kms from '@aws-cdk/aws-kms';
17+
import * as lambda from '@aws-cdk/aws-lambda';
1718
import * as s3 from '@aws-cdk/aws-s3';
19+
import * as targets from '@aws-cdk/aws-events-targets';
20+
import { Rule, Schedule } from '@aws-cdk/aws-events';
1821
import { RegionInfo } from '@aws-cdk/region-info';
1922
import { EbsDefaultEncryption } from '@aws-accelerator/custom-resource-ec2-ebs-default-encryption';
2023
import { S3PublicAccessBlock } from '@aws-accelerator/custom-resource-s3-public-access-block';
@@ -64,6 +67,7 @@ export async function step1(props: DefaultsStep1Props): Promise<DefaultsStep1Res
6467
const logAccountDefaultKeys = createDefaultEncryptionKeys(props);
6568
const accountEbsEncryptionKeys = createDefaultEbsEncryptionKey(props);
6669
const aesLogBucket = createAesLogBucket(props);
70+
6771
return {
6872
centralBucketCopy,
6973
centralLogBucket,
@@ -93,6 +97,10 @@ function blockS3PublicAccess(props: DefaultsStep1Props) {
9397
}
9498
}
9599

100+
/**
101+
* Creates a bucket that contains copies of the files in the central bucket.
102+
*/
103+
96104
/**
97105
* Creates a bucket that contains copies of the files in the central bucket.
98106
*/
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import { AccountStacks } from '../../common/account-stacks';
2+
import { Account } from '../../utils/accounts';
3+
import { AcceleratorConfig } from '@aws-accelerator/common-config/src';
4+
import { Organizations } from '@aws-accelerator/custom-resource-organization';
5+
import { createEncryptionKeyName } from '@aws-accelerator/cdk-accelerator/src/core/accelerator-name-generator';
6+
import * as targets from '@aws-cdk/aws-events-targets';
7+
import { Rule, Schedule } from '@aws-cdk/aws-events';
8+
import * as kms from '@aws-cdk/aws-kms';
9+
import * as iam from '@aws-cdk/aws-iam';
10+
import * as lambda from '@aws-cdk/aws-lambda';
11+
import * as s3 from '@aws-cdk/aws-s3';
12+
import * as cdk from '@aws-cdk/core';
13+
import path from 'path';
14+
import { getAccountId } from '@aws-accelerator/common-outputs/src/accounts';
15+
import { IamRoleNameOutputFinder } from '@aws-accelerator/common-outputs/src/iam-role';
16+
import { StackOutput } from '@aws-accelerator/common-outputs/src/stack-output';
17+
import { createPolicyName } from '@aws-accelerator/cdk-accelerator/src/core/accelerator-name-generator';
18+
export interface MetadataServiceProps {
19+
acceleratorPrefix: string;
20+
accountStacks: AccountStacks;
21+
accounts: Account[];
22+
config: AcceleratorConfig;
23+
configBucket: string;
24+
outputs: StackOutput[];
25+
}
26+
27+
export function createMetadataService(props: MetadataServiceProps) {
28+
const { accountStacks, config, acceleratorPrefix } = props;
29+
30+
const masterAccountConfig = config['global-options']['aws-org-management'];
31+
const logAccountConfig = config['global-options']['central-log-services'];
32+
33+
const masterAccountStack = accountStacks.getOrCreateAccountStack(masterAccountConfig.account);
34+
const logAccountStack = accountStacks.getOrCreateAccountStack(logAccountConfig.account);
35+
const organizations = new Organizations(logAccountStack, 'MetadataOrganizations');
36+
37+
const keyAlias = createEncryptionKeyName('Metadata-Key');
38+
const encryptionKey = new kms.Key(logAccountStack, 'MetadataBucketKey', {
39+
alias: `alias/${keyAlias}`,
40+
description: 'Key used to encrypt/decrypt the metadata S3 bucket',
41+
enableKeyRotation: true,
42+
});
43+
const bucketName = `${acceleratorPrefix.toLowerCase()}${getAccountId(
44+
props.accounts,
45+
logAccountConfig.account,
46+
)}-metadata-bucket`;
47+
const bucket = new s3.Bucket(logAccountStack, 'MetadataBucket', {
48+
encryptionKey,
49+
bucketName: bucketName,
50+
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
51+
removalPolicy: cdk.RemovalPolicy.RETAIN,
52+
objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_PREFERRED,
53+
versioned: true,
54+
});
55+
56+
// Let the bucket name be generated by CloudFormation
57+
// The generated bucket name is based on the stack name + logical ID + random suffix
58+
// overrideLogicalId(bucket, `metadata${masterAccountStack.region}`);
59+
60+
const anyAccountPrincipal = [new iam.AnyPrincipal()];
61+
62+
// Update Log Archive Bucket and KMS Key policies for roles with metadata service read only access
63+
const metadataReadOnlyRoles = [];
64+
for (const { accountKey, iam: iamConfig } of config.getIamConfigs()) {
65+
const accountId = getAccountId(props.accounts, accountKey);
66+
const roles = iamConfig.roles || [];
67+
for (const role of roles) {
68+
if (role['meta-data-read-only-access']) {
69+
metadataReadOnlyRoles.push(new iam.ArnPrincipal(`arn:aws:iam::${accountId}:role/${role.role}`));
70+
}
71+
}
72+
}
73+
// Give read only access to roles defined in config
74+
if (metadataReadOnlyRoles.length > 0) {
75+
encryptionKey.addToResourcePolicy(
76+
new iam.PolicyStatement({
77+
actions: ['kms:Decrypt', 'kms:DescribeKey', 'kms:GenerateDataKey'],
78+
principals: metadataReadOnlyRoles,
79+
resources: ['*'],
80+
}),
81+
);
82+
bucket.addToResourcePolicy(
83+
new iam.PolicyStatement({
84+
actions: ['s3:GetObject', 's3:ListBucket'],
85+
resources: [bucket.bucketArn, bucket.arnForObjects('*')],
86+
principals: metadataReadOnlyRoles,
87+
}),
88+
);
89+
}
90+
// Give all ASEA roles access to use this key for decryption
91+
encryptionKey.addToResourcePolicy(
92+
new iam.PolicyStatement({
93+
actions: ['kms:Decrypt', 'kms:GenerateDataKey'],
94+
principals: anyAccountPrincipal,
95+
resources: ['*'],
96+
conditions: {
97+
StringEquals: {
98+
'aws:PrincipalOrgID': organizations.organizationId,
99+
},
100+
ArnLike: {
101+
'aws:PrincipalARN': `arn:aws:iam::*:role/${acceleratorPrefix}*`,
102+
},
103+
},
104+
}),
105+
);
106+
107+
// Give all accounts access to get and list objects in this bucket
108+
bucket.addToResourcePolicy(
109+
new iam.PolicyStatement({
110+
actions: ['s3:Get*', 's3:List*'],
111+
resources: [bucket.bucketArn, bucket.arnForObjects('*')],
112+
principals: anyAccountPrincipal,
113+
conditions: {
114+
StringEquals: {
115+
'aws:PrincipalOrgID': organizations.organizationId,
116+
},
117+
ArnLike: {
118+
'aws:PrincipalARN': `arn:aws:iam::*:role/${acceleratorPrefix}*`,
119+
},
120+
},
121+
}),
122+
);
123+
124+
// Allow only https requests
125+
bucket.addToResourcePolicy(
126+
new iam.PolicyStatement({
127+
actions: ['s3:*'],
128+
resources: [bucket.bucketArn, bucket.arnForObjects('*')],
129+
principals: anyAccountPrincipal,
130+
conditions: {
131+
Bool: {
132+
'aws:SecureTransport': 'false',
133+
},
134+
},
135+
effect: iam.Effect.DENY,
136+
}),
137+
);
138+
139+
const lambdaPath = require.resolve('@aws-accelerator/deployments-runtime');
140+
const lambdaDir = path.dirname(lambdaPath);
141+
const lambdaCode = lambda.Code.fromAsset(lambdaDir);
142+
const lambdaRole = new iam.Role(masterAccountStack, 'metadata-lambda', {
143+
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
144+
roleName: `${props.acceleratorPrefix}metadata-lambda`,
145+
});
146+
147+
lambdaRole.addToPolicy(
148+
new iam.PolicyStatement({
149+
resources: ['*'],
150+
actions: [
151+
'codecommit:GetFile',
152+
'dynamodb:BatchGetItem',
153+
'dynamodb:GetRecords',
154+
'dynamodb:GetShardIterator',
155+
'dynamodb:Query',
156+
'dynamodb:GetItem',
157+
'dynamodb:Scan',
158+
'kms:DescribeKey',
159+
'kms:Decrypt',
160+
'kms:Encrypt',
161+
'kms:ReEncrypt*',
162+
'kms:GenerateDataKey*',
163+
'logs:CreateLogGroup',
164+
'logs:CreateLogStream',
165+
'logs:PutLogEvents',
166+
'organizations:Describe*',
167+
'organizations:List*',
168+
'organizations:Get*',
169+
'secretsmanager:Get*',
170+
'ssm:Get*',
171+
'states:List*',
172+
'sts:GetCallerIndentity',
173+
's3:*',
174+
],
175+
}),
176+
);
177+
178+
const metadataLambda = new lambda.Function(masterAccountStack, `MetadataLambda`, {
179+
runtime: lambda.Runtime.NODEJS_14_X,
180+
code: lambdaCode,
181+
role: lambdaRole,
182+
handler: 'index.metadataCollection',
183+
timeout: cdk.Duration.minutes(10),
184+
environment: {
185+
ACCELERATOR_PREFIX: props.acceleratorPrefix,
186+
BUCKET_NAME: bucketName,
187+
CONFIG_REPOSITORY_NAME: process.env.CONFIG_REPOSITORY_NAME || `${props.acceleratorPrefix}Config-Repo`,
188+
CENTRAL_BUCKET_NAME: props.configBucket,
189+
},
190+
});
191+
192+
encryptionKey.grantEncrypt(metadataLambda);
193+
bucket.grantWrite(metadataLambda);
194+
const cloudwatchrule = new Rule(masterAccountStack, `MetadataCWRule`, {
195+
schedule: Schedule.rate(cdk.Duration.days(1)),
196+
});
197+
198+
cloudwatchrule.addTarget(new targets.LambdaFunction(metadataLambda));
199+
200+
const iamConfig = config.getIamConfigs();
201+
for (const config of iamConfig) {
202+
const accountKey = config.accountKey;
203+
const metadataroles = config.iam.roles?.filter(role => {
204+
return role['meta-data-read-only-access'];
205+
});
206+
207+
const roleOutputs = metadataroles?.map(role => {
208+
return IamRoleNameOutputFinder.tryFindOneByName({
209+
outputs: props.outputs,
210+
accountKey,
211+
roleName: role.role,
212+
roleKey: 'IamAccountRole',
213+
});
214+
});
215+
const stackToUpdate = accountStacks.tryGetOrCreateAccountStack(
216+
accountKey,
217+
props.config['global-options']['central-log-services'].region,
218+
);
219+
220+
if (roleOutputs && roleOutputs.length > 0 && stackToUpdate) {
221+
for (const output of roleOutputs) {
222+
const policyName = createPolicyName('MetadataReadOnlyPolicy');
223+
const metadataPolicy = new iam.ManagedPolicy(stackToUpdate, `IAM-Metadata-Policy-${accountKey}`, {
224+
managedPolicyName: policyName,
225+
description: policyName,
226+
});
227+
228+
metadataPolicy.addStatements(
229+
new iam.PolicyStatement({
230+
effect: iam.Effect.ALLOW,
231+
actions: ['kms:Decrypt', 'kms:DescribeKey', 'kms:GenerateDataKey'],
232+
resources: ['*'],
233+
}),
234+
235+
new iam.PolicyStatement({
236+
effect: iam.Effect.ALLOW,
237+
actions: ['s3:GetObject', 's3:ListBucket'],
238+
resources: [`arn:aws:s3:::${bucketName}`, `arn:aws:s3:::${bucketName}/*`],
239+
}),
240+
);
241+
if (output) {
242+
metadataPolicy.attachToRole(iam.Role.fromRoleArn(stackToUpdate, 'metadataRole', output.roleArn));
243+
}
244+
}
245+
}
246+
}
247+
return bucket;
248+
}

src/deployments/runtime/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ export { handler as albIpMonitor } from './alb-to-alb-target/alb-ip-monitor';
1717
export { handler as albTargetRecordMonitor } from './alb-to-alb-target/alb-target-record-monitor';
1818
export { handler as firehoseCustomPrefix } from './firehose-custom-prefix/process-record';
1919
export { handler as eventToCWLPublisher } from './event-publish-cloudwatch-logs';
20+
export { handler as metadataCollection } from './metadata-collection';
2021
import * as ouValidationEvents from './ou-validation-events';
2122
export { ouValidationEvents };

0 commit comments

Comments
 (0)