Skip to content

Commit 2679c11

Browse files
authored
Add ssm-log-archive-read-only-access flag for IAM roles (#543) (#589)
Authored-by: Brian Fanning <bfannin@amazon.com>
1 parent e6dd868 commit 2679c11

File tree

16 files changed

+554
-18
lines changed

16 files changed

+554
-18
lines changed

src/core/runtime/src/save-outputs-to-ssm/iam-utils.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,15 +249,17 @@ export async function saveIamPolicy(
249249
}
250250
}
251251

252-
const ssmPolicyLength = iamConfig.roles?.filter(r => r['ssm-log-archive-access']).length;
252+
const ssmPolicyLength = iamConfig.roles?.filter(
253+
r => r['ssm-log-archive-write-access'] || r['ssm-log-archive-access'],
254+
).length;
253255
if (ssmPolicyLength && ssmPolicyLength !== 0) {
254256
const ssmPolicyOutput = IamPolicyOutputFinder.findOneByName({
255257
outputs,
256258
accountKey,
257-
policyKey: 'IamSsmAccessPolicy',
259+
policyKey: 'IamSsmWriteAccessPolicy',
258260
});
259261
if (!ssmPolicyOutput) {
260-
console.warn(`Didn't find IAM SSM Log Archive Access Policy in output`);
262+
console.warn(`Didn't find IAM SSM Log Archive Write Access Policy in output`);
261263
continue;
262264
}
263265
let currentIndex: number;

src/deployments/cdk/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
"@aws-accelerator/custom-resource-disassociate-hosted-zones": "workspace:^0.0.1",
101101
"@aws-accelerator/custom-resource-ec2-modify-transit-gateway-vpc-attachment": "workspace:^0.0.1",
102102
"@aws-accelerator/custom-resource-s3-put-bucket-versioning": "workspace:^0.0.1",
103+
"@aws-accelerator/custom-resource-s3-update-logarchive-policy": "workspace:^0.0.1",
103104
"@aws-cdk/aws-accessanalyzer": "1.85.0",
104105
"@aws-cdk/aws-autoscaling": "1.85.0",
105106
"@aws-cdk/aws-budgets": "1.85.0",

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import * as guardDutyDeployment from '../deployments/guardduty';
2727
import * as snsDeployment from '../deployments/sns';
2828
import * as ssmDeployment from '../deployments/ssm';
2929
import { getStackJsonOutput } from '@aws-accelerator/common-outputs/src/stack-output';
30+
import { logArchiveReadOnlyAccess } from '../deployments/s3/log-archive-read-access';
3031

3132
/**
3233
* This is the main entry point to deploy phase 2
@@ -345,6 +346,14 @@ export async function deploy({ acceleratorConfig, accountStacks, accounts, conte
345346
outputs,
346347
});
347348

349+
await logArchiveReadOnlyAccess({
350+
accountStacks,
351+
accounts,
352+
logBucket,
353+
config: acceleratorConfig,
354+
acceleratorPrefix: context.acceleratorPrefix,
355+
});
356+
348357
await tgwDeployment.acceptPeeringAttachment({
349358
accountStacks,
350359
accounts,

src/deployments/cdk/src/common/iam-assets.ts

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,18 @@ export class IamAssets extends cdk.Construct {
119119
}
120120
};
121121

122-
const createIamSSMLogArchivePolicy = (): iam.ManagedPolicy => {
123-
const policyName = createPolicyName('SSMAccessPolicy');
124-
const iamSSMLogArchiveAccessPolicy = new iam.ManagedPolicy(this, `IAM-SSM-LogArchive-Policy-${accountKey}`, {
125-
managedPolicyName: policyName,
126-
description: policyName,
127-
});
122+
const createIamSSMLogArchiveWritePolicy = (): iam.ManagedPolicy => {
123+
const policyName = createPolicyName('SSMWriteAccessPolicy');
124+
const iamSSMLogArchiveWriteAccessPolicy = new iam.ManagedPolicy(
125+
this,
126+
`IAM-SSM-LogArchive-Write-Policy-${accountKey}`,
127+
{
128+
managedPolicyName: policyName,
129+
description: policyName,
130+
},
131+
);
128132

129-
iamSSMLogArchiveAccessPolicy.addStatements(
133+
iamSSMLogArchiveWriteAccessPolicy.addStatements(
130134
new iam.PolicyStatement({
131135
effect: iam.Effect.ALLOW,
132136
actions: ['kms:DescribeKey', 'kms:GenerateDataKey*', 'kms:Decrypt', 'kms:Encrypt', 'kms:ReEncrypt*'],
@@ -152,11 +156,43 @@ export class IamAssets extends cdk.Construct {
152156
}),
153157
);
154158
new CfnIamPolicyOutput(this, `IamSsmPolicyOutput`, {
155-
policyName: iamSSMLogArchiveAccessPolicy.managedPolicyName,
156-
policyArn: iamSSMLogArchiveAccessPolicy.managedPolicyArn,
157-
policyKey: 'IamSsmAccessPolicy',
159+
policyName: iamSSMLogArchiveWriteAccessPolicy.managedPolicyName,
160+
policyArn: iamSSMLogArchiveWriteAccessPolicy.managedPolicyArn,
161+
policyKey: 'IamSsmWriteAccessPolicy',
158162
});
159-
return iamSSMLogArchiveAccessPolicy;
163+
return iamSSMLogArchiveWriteAccessPolicy;
164+
};
165+
166+
const createIamSSMLogArchiveReadOnlyPolicy = (): iam.ManagedPolicy => {
167+
const policyName = createPolicyName('SSMReadOnlyAccessPolicy');
168+
const iamSSMLogArchiveReadOnlyAccessPolicy = new iam.ManagedPolicy(
169+
this,
170+
`IAM-SSM-LogArchive-ReadOnly-Policy-${accountKey}`,
171+
{
172+
managedPolicyName: policyName,
173+
description: policyName,
174+
},
175+
);
176+
177+
iamSSMLogArchiveReadOnlyAccessPolicy.addStatements(
178+
new iam.PolicyStatement({
179+
effect: iam.Effect.ALLOW,
180+
actions: ['kms:Decrypt', 'kms:DescribeKey', 'kms:GenerateDataKey'],
181+
resources: [logBucket.encryptionKey?.keyArn || '*'],
182+
}),
183+
184+
new iam.PolicyStatement({
185+
effect: iam.Effect.ALLOW,
186+
actions: ['s3:GetObject'],
187+
resources: [logBucket.arnForObjects('*')],
188+
}),
189+
);
190+
new CfnIamPolicyOutput(this, `IamSsmReadOnlyPolicyOutput`, {
191+
policyName: iamSSMLogArchiveReadOnlyAccessPolicy.managedPolicyName,
192+
policyArn: iamSSMLogArchiveReadOnlyAccessPolicy.managedPolicyArn,
193+
policyKey: 'IamSsmReadOnlyAccessPolicy',
194+
});
195+
return iamSSMLogArchiveReadOnlyAccessPolicy;
160196
};
161197

162198
if (!IamConfigType.is(iamConfig)) {
@@ -191,8 +227,15 @@ export class IamAssets extends cdk.Construct {
191227
return;
192228
}
193229

194-
const ssmLogArchivePolicy =
195-
iamRoles.filter(i => i['ssm-log-archive-access']).length > 0 ? createIamSSMLogArchivePolicy() : undefined;
230+
const ssmLogArchiveWritePolicy =
231+
iamRoles.filter(i => i['ssm-log-archive-write-access'] || i['ssm-log-archive-access']).length > 0
232+
? createIamSSMLogArchiveWritePolicy()
233+
: undefined;
234+
235+
const ssmLogArchiveReadOnlyPolicy =
236+
iamRoles.filter(i => i['ssm-log-archive-read-only-access']).length > 0
237+
? createIamSSMLogArchiveReadOnlyPolicy()
238+
: undefined;
196239

197240
for (const iamRole of iamRoles) {
198241
if (!IamRoleConfigType.is(iamRole)) {
@@ -224,8 +267,15 @@ export class IamAssets extends cdk.Construct {
224267
roleKey: 'IamAccountRole',
225268
});
226269

227-
if (iamRole['ssm-log-archive-access'] && ssmLogArchivePolicy) {
228-
role.addManagedPolicy(ssmLogArchivePolicy);
270+
if (
271+
(iamRole['ssm-log-archive-write-access'] || iamRole['ssm-log-archive-access']) &&
272+
ssmLogArchiveWritePolicy
273+
) {
274+
role.addManagedPolicy(ssmLogArchiveWritePolicy);
275+
}
276+
277+
if (iamRole['ssm-log-archive-read-only-access'] && ssmLogArchiveReadOnlyPolicy) {
278+
role.addManagedPolicy(ssmLogArchiveReadOnlyPolicy);
229279
}
230280
}
231281
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './log-archive-read-access';
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import * as s3 from '@aws-cdk/aws-s3';
2+
import * as iam from '@aws-cdk/aws-iam';
3+
import { AccountStacks } from '../../common/account-stacks';
4+
import { AcceleratorConfig } from '@aws-accelerator/common-config/src';
5+
import { Account, getAccountId } from '@aws-accelerator/common-outputs/src/accounts';
6+
import { S3UpdateLogArchivePolicy } from '@aws-accelerator/custom-resource-s3-update-logarchive-policy';
7+
8+
export interface LogArchiveReadAccessProps {
9+
accountStacks: AccountStacks;
10+
accounts: Account[];
11+
logBucket: s3.IBucket;
12+
config: AcceleratorConfig;
13+
acceleratorPrefix: string;
14+
}
15+
16+
export async function logArchiveReadOnlyAccess(props: LogArchiveReadAccessProps) {
17+
const { accountStacks, accounts, logBucket, config, acceleratorPrefix } = props;
18+
const logArchiveAccountKey = config['global-options']['central-log-services'].account;
19+
const logArchiveStack = accountStacks.getOrCreateAccountStack(logArchiveAccountKey);
20+
const logArchiveReadOnlyRoles = [];
21+
22+
// Update Log Archive Bucket and KMS Key policies for roles with ssm-log-archive-read-only-access
23+
for (const { accountKey, iam: iamConfig } of config.getIamConfigs()) {
24+
const accountId = getAccountId(accounts, accountKey);
25+
const roles = iamConfig.roles || [];
26+
for (const role of roles) {
27+
if (role['ssm-log-archive-read-only-access']) {
28+
logArchiveReadOnlyRoles.push(`arn:aws:iam::${accountId}:role/${role.role}`);
29+
}
30+
}
31+
}
32+
33+
const LogBucketPolicy = new S3UpdateLogArchivePolicy(logArchiveStack, 'UpdateLogArchivePolicy', {
34+
roles: logArchiveReadOnlyRoles,
35+
logBucket,
36+
acceleratorPrefix,
37+
});
38+
}

src/lib/common-config/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,8 @@ export const IamPolicyConfigType = t.interface({
242242
policy: NonEmptyString,
243243
});
244244

245+
// ssm-log-archive-access will be deprecated in a future release.
246+
// ssm-log-archive-write-access should be used instead
245247
export const IamRoleConfigType = t.interface({
246248
role: NonEmptyString,
247249
type: NonEmptyString,
@@ -251,6 +253,8 @@ export const IamRoleConfigType = t.interface({
251253
'source-account-role': optional(t.string),
252254
'trust-policy': optional(t.string),
253255
'ssm-log-archive-access': optional(t.boolean),
256+
'ssm-log-archive-write-access': optional(t.boolean),
257+
'ssm-log-archive-read-only-access': optional(t.boolean),
254258
});
255259

256260
export const IamConfigType = t.interface({
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# S3 Update LogArchive Policy
2+
3+
This is a custom resource to grant any roles with 'ssm-log-archive-read-only-access: true' read access to the Log Archive Bucket and its corresponding KMS key
4+
5+
## Usage
6+
7+
import { S3UpdateLogArchivePolicy } from '@aws-accelerator/custom-resource-s3-update-logarchive-policy';
8+
9+
new S3UpdateLogArchivePolicy(scope, `UpdateLogArchivePolicy`, {
10+
roles: string[],
11+
logBucket: s3.IBucket,
12+
removalPolicy?: cdk.RemovalPolicy;,
13+
acceleratorPrefix: string
14+
});
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import * as path from 'path';
2+
import * as cdk from '@aws-cdk/core';
3+
import * as iam from '@aws-cdk/aws-iam';
4+
import * as lambda from '@aws-cdk/aws-lambda';
5+
import * as s3 from '@aws-cdk/aws-s3';
6+
import { HandlerProperties } from '@aws-accelerator/custom-resource-s3-update-logarchive-policy-runtime';
7+
8+
const resourceType = 'Custom::S3UpdateLogArchivePolicy';
9+
10+
export interface LogArchiveReadAccessProps {
11+
roles: string[];
12+
logBucket: s3.IBucket;
13+
removalPolicy?: cdk.RemovalPolicy;
14+
acceleratorPrefix: string;
15+
}
16+
17+
/**
18+
* Adds IAM roles with {'ssm-log-archive-read-only-access': true} to the LogArchive bucket policy
19+
*/
20+
export class S3UpdateLogArchivePolicy extends cdk.Construct {
21+
private resource: cdk.CustomResource | undefined;
22+
23+
constructor(scope: cdk.Construct, id: string, private readonly props: LogArchiveReadAccessProps) {
24+
super(scope, id);
25+
26+
const { roles, logBucket, acceleratorPrefix } = props;
27+
}
28+
29+
get role(): iam.IRole {
30+
return this.lambdaFunction.role!;
31+
}
32+
33+
protected onPrepare() {
34+
const handlerProperties: HandlerProperties = {
35+
roles: this.props.roles,
36+
logBucketArn: this.props.logBucket.bucketArn,
37+
logBucketName: this.props.logBucket.bucketName,
38+
logBucketKmsKeyArn: this.props.logBucket.encryptionKey?.keyArn,
39+
};
40+
41+
this.resource = new cdk.CustomResource(this, 'Resource', {
42+
resourceType,
43+
serviceToken: this.lambdaFunction.functionArn,
44+
removalPolicy: this.props.removalPolicy ?? cdk.RemovalPolicy.DESTROY,
45+
properties: handlerProperties,
46+
});
47+
}
48+
49+
private get lambdaFunction(): lambda.Function {
50+
const constructName = `${resourceType}Lambda`;
51+
const stack = cdk.Stack.of(this);
52+
const existing = stack.node.tryFindChild(constructName);
53+
if (existing) {
54+
return existing as lambda.Function;
55+
}
56+
57+
const lambdaPath = require.resolve('@aws-accelerator/custom-resource-s3-update-logarchive-policy-runtime');
58+
const lambdaDir = path.dirname(lambdaPath);
59+
60+
const role = new iam.Role(stack, 'Role', {
61+
roleName: `${this.props.acceleratorPrefix}S3UpdateLogArchivePolicy`,
62+
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
63+
});
64+
65+
role.addToPrincipalPolicy(
66+
new iam.PolicyStatement({
67+
actions: [
68+
's3:GetBucketPolicy',
69+
's3:PutBucketPolicy',
70+
'kms:GetKeyPolicy',
71+
'kms:PutKeyPolicy',
72+
'logs:CreateLogGroup',
73+
'logs:CreateLogStream',
74+
'logs:PutLogEvents',
75+
'tag:GetResources',
76+
],
77+
resources: ['*'],
78+
}),
79+
);
80+
81+
return new lambda.Function(stack, constructName, {
82+
runtime: lambda.Runtime.NODEJS_12_X,
83+
code: lambda.Code.fromAsset(lambdaDir),
84+
handler: 'index.handler',
85+
role,
86+
timeout: cdk.Duration.seconds(30),
87+
});
88+
}
89+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "@aws-accelerator/custom-resource-s3-update-logarchive-policy",
3+
"peerDependencies": {
4+
"@aws-cdk/aws-iam": "1.85.0",
5+
"@aws-cdk/core": "1.85.0",
6+
"@aws-cdk/aws-lambda": "1.85.0"
7+
},
8+
"main": "cdk/index.ts",
9+
"private": true,
10+
"version": "0.0.1",
11+
"dependencies": {
12+
"@aws-cdk/aws-iam": "1.85.0",
13+
"@aws-cdk/core": "1.85.0",
14+
"@aws-cdk/aws-lambda": "1.85.0",
15+
"@aws-cdk/aws-s3": "1.85.0"
16+
},
17+
"devDependencies": {
18+
"@aws-accelerator/custom-resource-s3-update-logarchive-policy-runtime": "workspace:^0.0.1",
19+
"eslint": "7.10.0",
20+
"@typescript-eslint/eslint-plugin": "4.4.0",
21+
"@typescript-eslint/parser": "4.4.0",
22+
"ts-node": "6.2.0",
23+
"eslint-config-standard": "14.1.1",
24+
"eslint-plugin-standard": "4.0.1",
25+
"eslint-plugin-deprecation": "1.1.0",
26+
"eslint-plugin-promise": "4.2.1",
27+
"eslint-plugin-import": "2.22.1",
28+
"eslint-plugin-node": "11.1.0",
29+
"eslint-plugin-jsdoc": "30.6.4",
30+
"eslint-plugin-prefer-arrow": "1.2.2",
31+
"eslint-plugin-react": "7.21.3",
32+
"eslint-plugin-unicorn": "22.0.0",
33+
"eslint-config-prettier": "6.12.0",
34+
"typescript": "3.8.3",
35+
"@types/node": "12.12.6"
36+
}
37+
}

0 commit comments

Comments
 (0)