Skip to content

Commit f7a5c5d

Browse files
committed
Assign least-privilege IAM policies to pipeline
1 parent 45bf540 commit f7a5c5d

14 files changed

+719
-76
lines changed

cloud/README.md

Lines changed: 82 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Generates the CloudFormation templates for a "dev stage" build pipeline.
2828
Generates the CloudFormation templates for a "production stage" build pipeline.
2929

3030
`npm run cdk:synth -- --context STAGE={STAGENAME}`
31-
Generates the CloudFormation templates for a build pipeline, stage given by `{STAGENAME}`.
31+
Generates the CloudFormation templates for a build pipeline, for the given stage name
3232

3333
`npm run cdk:deploy:all`
3434
Deploys the synthesized pipeline to an AWS Environment, as defined by your active AWS config profile.
@@ -54,8 +54,8 @@ costs.
5454

5555
`npm run cdk:test:deploy` - deploys these stacks to AWS as "dev" stage
5656

57-
All being successful, you should see the application login screen at `https://dev.spylogic.ai`. Log into the AWS Console to add a
58-
user to the dev Cognito userpool, then log into the UI to test app deployment was successful.
57+
All being successful, you should see the application login screen at `https://dev.spylogic.ai`. Log into the AWS Console
58+
to add a user to the dev Cognito userpool, then log into the UI to test app deployment was successful.
5959

6060
`npm run cdk:test:destroy` - Remember to destroy the stacks after testing, else you will rack up costs!
6161

@@ -66,52 +66,115 @@ user to the dev Cognito userpool, then log into the UI to test app deployment wa
6666
At the time of writing, current infrastructure costs us around $60 per month, with just two AZs for the load balancer,
6767
deployed into `eu-north-1`. This is one of the [greenest AWS regions](https://app.electricitymaps.com/map), but costs
6868
are about average. The vast majority of the bill is for the VPC, Load Balancer and NAT EC2 Instance. We have tasks on
69-
our todo list to reduce these costs (such as removing the NAT Instance in favour of IPv6 egress), but those are
70-
work-in-progress.
69+
our todo list to reduce these costs (radical idea: convert container app to REDIS-backed lambdas).
7170

7271
The bottom line: remember to destroy your stacks when no longer needed!
7372

7473
## First-time admin tasks
7574

7675
When setting up the CDK project for the first time, there are a few one-time tasks you must complete.
7776

78-
### Bootstrapping the CDK using a Developer Policy
77+
### Bootstrapping the CDK
7978

8079
In order to deploy AWS resources to a remote environment using CDK, you must first
8180
[bootstrap the CDK](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html). For this project, as per
8281
[CDK guidelines](https://aws.amazon.com/blogs/devops/secure-cdk-deployments-with-iam-permission-boundaries/), we are
8382
using a lightweight permissions boundary to restrict permissions, to prevent creation of new users or roles with
8483
elevated permissions. See `cdk-developer-policy.yaml` for details.
8584

86-
Note that once the pipeline is deployed, it is a good idea to restrict permissions further, so that only the pipeline
87-
can make changes to the stacks.
85+
We also have a set of [IAM Managed Policies](./permissions/README.md) that restrict what CloudFormation is allowed to
86+
do, as CDK by default allows full AdministratorAccess! 😵 🤢 🤮
8887

89-
Create the permissions boundary CloudFormation stack:
88+
Note that once the pipeline is deployed, it's a good idea to restrict permissions for developers further, so that only
89+
the pipeline can make changes to the stacks, via approved GitHub merges.
9090

91-
```
91+
1. Create permissions boundary stack
92+
93+
```shell
9294
aws cloudformation create-stack \
9395
--stack-name CDKDeveloperPolicy \
9496
--template-body file://cdk-developer-policy.yaml \
9597
--capabilities CAPABILITY_NAMED_IAM
9698
```
9799

98-
Then bootstrap the CDK:
99-
100+
2. Create IAM managed policies
101+
102+
```shell
103+
aws iam create-policy \
104+
--policy-name cdk-execution-policy-basics \
105+
--policy-document file://permissions/execution_policy_basics.json \
106+
--description "Baseline permissions for cloudformation deployments"
107+
108+
aws iam create-policy \
109+
--policy-name cdk-execution-policy-cloudfront \
110+
--policy-document file://permissions/execution_policy_cloudfront.json \
111+
--description "Permissions to deploy cloudfront resources, except for lambda@edge functions"
112+
113+
aws iam create-policy \
114+
--policy-name cdk-execution-policy-cognito \
115+
--policy-document file://permissions/execution_policy_cognito.json \
116+
--description "Permissions to deploy cognito userpools and related resources"
117+
118+
aws iam create-policy \
119+
--policy-name cdk-execution-policy-edgelambda \
120+
--policy-document file://permissions/execution_policy_edgelambda.json \
121+
--description "Permissions to deploy lambda@edge functions for a cloudfront distribution"
122+
123+
aws iam create-policy \
124+
--policy-name cdk-execution-policy-pipeline \
125+
--policy-document file://permissions/execution_policy_pipeline.json \
126+
--description "Permissions to deploy a codepipeline and codebuild projects"
127+
128+
aws iam create-policy \
129+
--policy-name cdk-execution-policy-route53 \
130+
--policy-document file://permissions/execution_policy_route53.json \
131+
--description "Permissions to deploy domain records and ACM certificates"
132+
133+
aws iam create-policy \
134+
--policy-name cdk-execution-policy-vpc \
135+
--policy-document file://permissions/execution_policy_vpc.json \
136+
--description "Permissions to deploy VPC, EC2 and ECS resources for a Fargate-managed container app"
100137
```
101-
# install dependencies if not already done
102-
npm install
103138

104-
# run the bootstrap command
105-
npx cdk bootstrap --custom-permissions-boundary cdk-developer-policy
139+
3. Bootstrap the CDK environment
140+
141+
```shell
142+
# install dependencies if you've not already done so
143+
npm install
106144
```
107145

108-
Unless your default region is `us-east-1`, you will also need to bootstrap this region, as certificates for CloudFront
109-
currently need to be deployed there:
146+
If your primary region is NOT `us-east-1`, you will need to bootstrap that region as well, as
147+
currently Lambda@Edge functions can only be deployed to `us-east-1`:
110148

149+
```shell
150+
# Bootstrap primary region
151+
npx cdk bootstrap aws://{account}/{region} \
152+
--custom-permissions-boundary cdk-developer-policy \
153+
--cloudformation-execution-policies "arn:aws:iam::{account}:policy/cdk-execution-policy-basics,arn:aws:iam::{account}:policy/cdk-execution-policy-cloudfront,arn:aws:iam::{account}:policy/cdk-execution-policy-cognito,arn:aws:iam::{account}:policy/cdk-execution-policy-pipeline,arn:aws:iam::{account}:policy/cdk-execution-policy-route53,arn:aws:iam::{account}:policy/cdk-execution-policy-vpc"
154+
155+
# Bootstrap us-east-1 for cloudfront
156+
npx cdk bootstrap aws://{account}/us-east-1 \
157+
--custom-permissions-boundary cdk-developer-policy \
158+
--cloudformation-execution-policies "arn:aws:iam::{account}:policy/cdk-execution-policy-basics,arn:aws:iam::{account}:policy/cdk-execution-policy-edgelambda"
111159
```
112-
npx cdk bootstrap --custom-permissions-boundary cdk-developer-policy aws://YOUR_ACCOUNT_NUMBER/us-east-1
160+
161+
If your primary region IS `us-east-1`, then you only need one bootstrap command:
162+
163+
```shell
164+
# Bootstrap us-east-1
165+
npx cdk bootstrap aws://{account}/us-east-1 \
166+
--custom-permissions-boundary cdk-developer-policy \
167+
--cloudformation-execution-policies "arn:aws:iam::{account}:policy/cdk-execution-policy-basics,arn:aws:iam::{account}:policy/cdk-execution-policy-cloudfront,arn:aws:iam::{account}:policy/cdk-execution-policy-cognito,arn:aws:iam::{account}:policy/cdk-execution-policy-edgelambda,arn:aws:iam::{account}:policy/cdk-execution-policy-pipeline,arn:aws:iam::{account}:policy/cdk-execution-policy-route53,arn:aws:iam::{account}:policy/cdk-execution-policy-vpc"
113168
```
114169

170+
### SSM Parameters
171+
172+
There are two Parameters needed when the stacks are deployed, so ensure these are added before you deploy the
173+
pipeline first time:
174+
175+
- `DOMAIN_NAME` - Domain where the application will be available
176+
- `HOSTED_ZONE_ID` - We advise you create your Hosted Zone manually (via the AWS Console) before deploying the stacks
177+
115178
### Server secrets
116179

117180
The Node Express server needs a couple of secret values, which are injected into the container environment during

cloud/bin/pipeline.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,23 @@ const tags = {
2525
stage: stageName(app),
2626
};
2727

28+
const generateStackName = stackName(app);
29+
const generateDescription = resourceDescription(app);
30+
2831
/* Pipeline is now responsible for deploying all other Stacks */
2932

30-
const pipelineUsEast1Stack = new PipelineAssistUsEast1Stack(
31-
app,
32-
stackName(app)('pipeline-useast1'),
33-
{
34-
description: resourceDescription(app)('Code Pipeline Cross-Region resources stack (us-east-1)'),
35-
env,
36-
tags,
37-
}
38-
);
39-
40-
new PipelineStack(app, stackName(app)('pipeline'), {
41-
description: resourceDescription(app)('Code Pipeline stack'),
33+
const pipelineUsEast1StackName = generateStackName('pipeline-useast1');
34+
const pipelineUsEast1Stack = new PipelineAssistUsEast1Stack(app, pipelineUsEast1StackName, {
35+
stackName: pipelineUsEast1StackName,
36+
description: generateDescription('Code Pipeline Cross-Region resources stack (us-east-1)'),
37+
env,
38+
tags,
39+
});
40+
41+
const pipelineStackName = generateStackName('pipeline');
42+
new PipelineStack(app, pipelineStackName, {
43+
stackName: pipelineStackName,
44+
description: generateDescription('Code Pipeline stack'),
4245
env,
4346
tags,
4447
usEast1Bucket: pipelineUsEast1Stack.resourceBucket,

cloud/lib/app-stage.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,28 +30,36 @@ export class AppStage extends Stage {
3030
const generateDescription = resourceDescription(scope);
3131
const generateStackName = stackName(scope);
3232

33-
const hostedZoneStack = new HostedZoneStack(this, generateStackName('hostedzone'), {
33+
const hostedZoneStackName = generateStackName('hostedzone');
34+
const hostedZoneStack = new HostedZoneStack(this, hostedZoneStackName, {
35+
stackName: hostedZoneStackName,
3436
description: generateDescription('Hosted Zone stack'),
3537
env,
3638
tags,
3739
});
3840

39-
const certificateStack = new CertificateStack(this, generateStackName('certificate'), {
41+
const certificateStackName = generateStackName('certificate');
42+
const certificateStack = new CertificateStack(this, certificateStackName, {
43+
stackName: certificateStackName,
4044
description: generateDescription('Certificate stack'),
4145
env,
4246
tags,
4347
domainName: hostedZoneStack.topLevelDomain.value as string,
4448
hostedZone: hostedZoneStack.hostedZone,
4549
});
4650

47-
const authStack = new AuthStack(this, generateStackName('auth'), {
51+
const authStackName = generateStackName('auth');
52+
const authStack = new AuthStack(this, authStackName, {
53+
stackName: authStackName,
4854
description: generateDescription('Auth stack'),
4955
env,
5056
tags,
5157
domainName: hostedZoneStack.topLevelDomain.value as string,
5258
});
5359

54-
new ApiStack(this, generateStackName('api'), {
60+
const apiStackName = generateStackName('api');
61+
new ApiStack(this, apiStackName, {
62+
stackName: apiStackName,
5563
description: generateDescription('API stack'),
5664
env,
5765
tags,
@@ -63,7 +71,9 @@ export class AppStage extends Stage {
6371
hostedZone: hostedZoneStack.hostedZone,
6472
});
6573

66-
const uiStack = new UiStack(this, generateStackName('ui'), {
74+
const uiStackName = generateStackName('ui');
75+
const uiStack = new UiStack(this, uiStackName, {
76+
stackName: uiStackName,
6777
description: generateDescription('UI stack'),
6878
env,
6979
tags,

cloud/lib/pipeline-stack.ts

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { BuildEnvironmentVariableType, BuildSpec } from 'aws-cdk-lib/aws-codebuild';
1+
import {
2+
BuildEnvironmentVariable,
3+
BuildEnvironmentVariableType,
4+
BuildSpec,
5+
} from 'aws-cdk-lib/aws-codebuild';
26
import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
37
import { IBucket } from 'aws-cdk-lib/aws-s3';
48
import { Stack, StackProps } from 'aws-cdk-lib/core';
@@ -25,18 +29,44 @@ export class PipelineStack extends Stack {
2529
const generateResourceId = resourceId(scope);
2630
const stage = stageName(scope);
2731

28-
const sourceCode = CodePipelineSource.connection('ScottLogic/prompt-injection', 'main', {
29-
connectionArn: `arn:aws:codestar-connections:${env.region}:${env.account}:connection/05c0f0a4-2233-4269-a697-33a339f8a6bc`,
30-
});
32+
// FIXME Reset branch to 'main' !!!
33+
const sourceCode = CodePipelineSource.connection(
34+
'ScottLogic/prompt-injection',
35+
'feature/aws-cloud-infrastructure',
36+
{
37+
//connectionArn: `arn:aws:codestar-connections:${env.region}:${env.account}:connection/05c0f0a4-2233-4269-a697-33a339f8a6bc`,
38+
connectionArn: `arn:aws:codestar-connections:eu-north-1:${env.account}:connection/05c0f0a4-2233-4269-a697-33a339f8a6bc`,
39+
}
40+
);
3141

3242
const hostBucketName = generateResourceId('host-bucket');
3343

44+
const identityProviderEnv: Record<string, BuildEnvironmentVariable> =
45+
process.env.IDP_NAME?.toUpperCase() === 'AZURE'
46+
? {
47+
IDP_NAME: {
48+
type: BuildEnvironmentVariableType.PLAINTEXT,
49+
value: 'AZURE',
50+
},
51+
AZURE_APPLICATION_ID: {
52+
type: BuildEnvironmentVariableType.PARAMETER_STORE,
53+
value: 'AZURE_APPLICATION_ID',
54+
},
55+
AZURE_TENANT_ID: {
56+
type: BuildEnvironmentVariableType.PARAMETER_STORE,
57+
value: 'AZURE_TENANT_ID',
58+
},
59+
}
60+
: {};
61+
3462
const pipeline = new CodePipeline(this, generateResourceId('pipeline'), {
3563
synth: new ShellStep('Synth', {
3664
input: sourceCode,
3765
installCommands: ['npm ci'],
38-
commands: ['cd cloud', `npm run cdk:synth -- --context STAGE=${stage}`],
39-
primaryOutputDirectory: 'cloud/cdk.out',
66+
// FIXME Revert this to `npm run cdk:synth -- --context STAGE=${stage}`
67+
commands: ['cd cloud', 'npm run cdk:dev:synth'],
68+
// FIXME Revert this to 'cloud/cdk.out'
69+
primaryOutputDirectory: 'cloud/cdk.dev.out',
4070
}),
4171
synthCodeBuildDefaults: {
4272
buildEnvironment: {
@@ -49,18 +79,7 @@ export class PipelineStack extends Stack {
4979
type: BuildEnvironmentVariableType.PARAMETER_STORE,
5080
value: 'HOSTED_ZONE_ID',
5181
},
52-
IDP_NAME: {
53-
type: BuildEnvironmentVariableType.PLAINTEXT,
54-
value: 'AZURE',
55-
},
56-
AZURE_APPLICATION_ID: {
57-
type: BuildEnvironmentVariableType.PARAMETER_STORE,
58-
value: 'AZURE_APPLICATION_ID',
59-
},
60-
AZURE_TENANT_ID: {
61-
type: BuildEnvironmentVariableType.PARAMETER_STORE,
62-
value: 'AZURE_TENANT_ID',
63-
},
82+
...identityProviderEnv,
6483
},
6584
},
6685
},
@@ -74,6 +93,8 @@ export class PipelineStack extends Stack {
7493

7594
// Pre-deployment quality checks
7695
deployment.addPre(
96+
// TODO Add a ConfirmPermissionsBroadening step:
97+
// new ConfirmPermissionsBroadening('Check Permissions', { stage: appStage }),
7798
new CodeBuildStep('API-CodeChecks', {
7899
input: sourceCode,
79100
commands: [

cloud/lib/ui-stack.ts

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -96,29 +96,27 @@ export class UiStack extends Stack {
9696
If so, we might be able to switch to a CloudFront Function instead of Edge,
9797
and use CloudFront KeyValueStore to hold our jwks value as JSON.
9898
*/
99-
const verifierEdgeFunction = new experimental.EdgeFunction(
100-
this,
101-
generateResourceId('api-gatekeeper'),
102-
{
103-
stackId: stackName(scope)('edge-lambda'),
104-
handler: 'index.handler',
105-
runtime: Runtime.NODEJS_18_X,
106-
code: new TypeScriptCode(join(__dirname, 'lambdas/verifyAuth/index.ts'), {
107-
buildOptions: {
108-
bundle: true,
109-
external: ['@aws-sdk/client-ssm'],
110-
minify: false,
111-
platform: 'node',
112-
target: 'node18',
113-
define: {
114-
'process.env.DOMAIN_NAME': `"${domainName}"`,
115-
'process.env.PARAM_USERPOOL_ID': `"${parameterNameUserPoolId}"`,
116-
'process.env.PARAM_USERPOOL_CLIENT': `"${parameterNameUserPoolClient}"`,
117-
},
99+
const edgeFunctionName = generateResourceId('api-gatekeeper');
100+
const verifierEdgeFunction = new experimental.EdgeFunction(this, edgeFunctionName, {
101+
stackId: stackName(scope)('edge-lambda'),
102+
functionName: edgeFunctionName,
103+
handler: 'index.handler',
104+
runtime: Runtime.NODEJS_18_X,
105+
code: new TypeScriptCode(join(__dirname, 'lambdas/verifyAuth/index.ts'), {
106+
buildOptions: {
107+
bundle: true,
108+
external: ['@aws-sdk/client-ssm'],
109+
minify: false,
110+
platform: 'node',
111+
target: 'node18',
112+
define: {
113+
'process.env.DOMAIN_NAME': `"${domainName}"`,
114+
'process.env.PARAM_USERPOOL_ID': `"${parameterNameUserPoolId}"`,
115+
'process.env.PARAM_USERPOOL_CLIENT': `"${parameterNameUserPoolClient}"`,
118116
},
119-
}),
120-
}
121-
);
117+
},
118+
}),
119+
});
122120
verifierEdgeFunction.addToRolePolicy(
123121
new PolicyStatement({
124122
effect: Effect.ALLOW,

cloud/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
"cdk:test:destroy": "cdk destroy --app cdk.test.out",
2020
"cdk:test:destroy:all": "cdk destroy --app cdk.test.out --all",
2121
"cdk:test:clean": "rimraf cdk.test.out",
22+
"cdk:dev:synth": "cdk synth -o cdk.dev.out --context STAGE=dev",
23+
"cdk:dev:deploy": "cdk deploy --app cdk.dev.out --all",
24+
"cdk:dev:destroy": "cdk destroy --app cdk.dev.out --all",
25+
"cdk:dev:clean": "rimraf cdk.dev.out",
2226
"codecheck": "concurrently \"npm run lint:check\" \"npm run format:check\"",
2327
"format": "prettier . --write",
2428
"format:check": "prettier . --check",

0 commit comments

Comments
 (0)