Skip to content

Commit

Permalink
Assign least-privilege IAM policies to pipeline
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswilty committed Sep 12, 2024
1 parent 45bf540 commit f19c9ab
Show file tree
Hide file tree
Showing 19 changed files with 1,102 additions and 57 deletions.
27 changes: 15 additions & 12 deletions cloud/bin/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,23 @@ const tags = {
stage: stageName(app),
};

const generateStackName = stackName(app);
const generateDescription = resourceDescription(app);

/* Pipeline is now responsible for deploying all other Stacks */

const pipelineUsEast1Stack = new PipelineAssistUsEast1Stack(
app,
stackName(app)('pipeline-useast1'),
{
description: resourceDescription(app)('Code Pipeline Cross-Region resources stack (us-east-1)'),
env,
tags,
}
);

new PipelineStack(app, stackName(app)('pipeline'), {
description: resourceDescription(app)('Code Pipeline stack'),
const pipelineUsEast1StackName = generateStackName('pipeline-useast1');
const pipelineUsEast1Stack = new PipelineAssistUsEast1Stack(app, pipelineUsEast1StackName, {
stackName: pipelineUsEast1StackName,
description: generateDescription('Code Pipeline Cross-Region resources stack (us-east-1)'),
env,
tags,
});

const pipelineStackName = generateStackName('pipeline');
new PipelineStack(app, pipelineStackName, {
stackName: pipelineStackName,
description: generateDescription('Code Pipeline stack'),
env,
tags,
usEast1Bucket: pipelineUsEast1Stack.resourceBucket,
Expand Down
20 changes: 15 additions & 5 deletions cloud/lib/app-stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,28 +30,36 @@ export class AppStage extends Stage {
const generateDescription = resourceDescription(scope);
const generateStackName = stackName(scope);

const hostedZoneStack = new HostedZoneStack(this, generateStackName('hostedzone'), {
const hostedZoneStackName = generateStackName('hostedzone');
const hostedZoneStack = new HostedZoneStack(this, hostedZoneStackName, {
stackName: hostedZoneStackName,
description: generateDescription('Hosted Zone stack'),
env,
tags,
});

const certificateStack = new CertificateStack(this, generateStackName('certificate'), {
const certificateStackName = generateStackName('certificate');
const certificateStack = new CertificateStack(this, certificateStackName, {
stackName: certificateStackName,
description: generateDescription('Certificate stack'),
env,
tags,
domainName: hostedZoneStack.topLevelDomain.value as string,
hostedZone: hostedZoneStack.hostedZone,
});

const authStack = new AuthStack(this, generateStackName('auth'), {
const authStackName = generateStackName('auth');
const authStack = new AuthStack(this, authStackName, {
stackName: authStackName,
description: generateDescription('Auth stack'),
env,
tags,
domainName: hostedZoneStack.topLevelDomain.value as string,
});

new ApiStack(this, generateStackName('api'), {
const apiStackName = generateStackName('api');
new ApiStack(this, apiStackName, {
stackName: apiStackName,
description: generateDescription('API stack'),
env,
tags,
Expand All @@ -63,7 +71,9 @@ export class AppStage extends Stage {
hostedZone: hostedZoneStack.hostedZone,
});

const uiStack = new UiStack(this, generateStackName('ui'), {
const uiStackName = generateStackName('ui');
const uiStack = new UiStack(this, uiStackName, {
stackName: uiStackName,
description: generateDescription('UI stack'),
env,
tags,
Expand Down
57 changes: 39 additions & 18 deletions cloud/lib/pipeline-stack.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { BuildEnvironmentVariableType, BuildSpec } from 'aws-cdk-lib/aws-codebuild';
import {
BuildEnvironmentVariable,
BuildEnvironmentVariableType,
BuildSpec,
} from 'aws-cdk-lib/aws-codebuild';
import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { IBucket } from 'aws-cdk-lib/aws-s3';
import { Stack, StackProps } from 'aws-cdk-lib/core';
Expand All @@ -25,18 +29,44 @@ export class PipelineStack extends Stack {
const generateResourceId = resourceId(scope);
const stage = stageName(scope);

const sourceCode = CodePipelineSource.connection('ScottLogic/prompt-injection', 'main', {
connectionArn: `arn:aws:codestar-connections:${env.region}:${env.account}:connection/05c0f0a4-2233-4269-a697-33a339f8a6bc`,
});
// FIXME Reset branch to 'main' !!!
const sourceCode = CodePipelineSource.connection(
'ScottLogic/prompt-injection',
'feature/aws-cloud-infrastructure',
{
//connectionArn: `arn:aws:codestar-connections:${env.region}:${env.account}:connection/05c0f0a4-2233-4269-a697-33a339f8a6bc`,
connectionArn: `arn:aws:codestar-connections:eu-north-1:${env.account}:connection/05c0f0a4-2233-4269-a697-33a339f8a6bc`,
}
);

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

const identityProviderEnv: Record<string, BuildEnvironmentVariable> =
process.env.IDP_NAME?.toUpperCase() === 'AZURE'
? {
IDP_NAME: {
type: BuildEnvironmentVariableType.PLAINTEXT,
value: 'AZURE',
},
AZURE_APPLICATION_ID: {
type: BuildEnvironmentVariableType.PARAMETER_STORE,
value: 'AZURE_APPLICATION_ID',
},
AZURE_TENANT_ID: {
type: BuildEnvironmentVariableType.PARAMETER_STORE,
value: 'AZURE_TENANT_ID',
},
}
: {};

const pipeline = new CodePipeline(this, generateResourceId('pipeline'), {
synth: new ShellStep('Synth', {
input: sourceCode,
installCommands: ['npm ci'],
commands: ['cd cloud', `npm run cdk:synth -- --context STAGE=${stage}`],
primaryOutputDirectory: 'cloud/cdk.out',
// FIXME Revert this to `npm run cdk:synth -- --context STAGE=${stage}`
commands: ['cd cloud', 'npm run cdk:dev:synth'],
// FIXME Revert this to 'cloud/cdk.out'
primaryOutputDirectory: 'cloud/cdk.dev.out',
}),
synthCodeBuildDefaults: {
buildEnvironment: {
Expand All @@ -49,18 +79,7 @@ export class PipelineStack extends Stack {
type: BuildEnvironmentVariableType.PARAMETER_STORE,
value: 'HOSTED_ZONE_ID',
},
IDP_NAME: {
type: BuildEnvironmentVariableType.PLAINTEXT,
value: 'AZURE',
},
AZURE_APPLICATION_ID: {
type: BuildEnvironmentVariableType.PARAMETER_STORE,
value: 'AZURE_APPLICATION_ID',
},
AZURE_TENANT_ID: {
type: BuildEnvironmentVariableType.PARAMETER_STORE,
value: 'AZURE_TENANT_ID',
},
...identityProviderEnv,
},
},
},
Expand All @@ -74,6 +93,8 @@ export class PipelineStack extends Stack {

// Pre-deployment quality checks
deployment.addPre(
// TODO Add a ConfirmPermissionsBroadening step:
// new ConfirmPermissionsBroadening('Check Permissions', { stage: appStage }),
new CodeBuildStep('API-CodeChecks', {
input: sourceCode,
commands: [
Expand Down
42 changes: 20 additions & 22 deletions cloud/lib/ui-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,29 +96,27 @@ export class UiStack extends Stack {
If so, we might be able to switch to a CloudFront Function instead of Edge,
and use CloudFront KeyValueStore to hold our jwks value as JSON.
*/
const verifierEdgeFunction = new experimental.EdgeFunction(
this,
generateResourceId('api-gatekeeper'),
{
stackId: stackName(scope)('edge-lambda'),
handler: 'index.handler',
runtime: Runtime.NODEJS_18_X,
code: new TypeScriptCode(join(__dirname, 'lambdas/verifyAuth/index.ts'), {
buildOptions: {
bundle: true,
external: ['@aws-sdk/client-ssm'],
minify: false,
platform: 'node',
target: 'node18',
define: {
'process.env.DOMAIN_NAME': `"${domainName}"`,
'process.env.PARAM_USERPOOL_ID': `"${parameterNameUserPoolId}"`,
'process.env.PARAM_USERPOOL_CLIENT': `"${parameterNameUserPoolClient}"`,
},
const edgeFunctionName = generateResourceId('api-gatekeeper');
const verifierEdgeFunction = new experimental.EdgeFunction(this, edgeFunctionName, {
stackId: stackName(scope)('edge-lambda'),
functionName: edgeFunctionName,
handler: 'index.handler',
runtime: Runtime.NODEJS_18_X,
code: new TypeScriptCode(join(__dirname, 'lambdas/verifyAuth/index.ts'), {
buildOptions: {
bundle: true,
external: ['@aws-sdk/client-ssm'],
minify: false,
platform: 'node',
target: 'node18',
define: {
'process.env.DOMAIN_NAME': `"${domainName}"`,
'process.env.PARAM_USERPOOL_ID': `"${parameterNameUserPoolId}"`,
'process.env.PARAM_USERPOOL_CLIENT': `"${parameterNameUserPoolClient}"`,
},
}),
}
);
},
}),
});
verifierEdgeFunction.addToRolePolicy(
new PolicyStatement({
effect: Effect.ALLOW,
Expand Down
4 changes: 4 additions & 0 deletions cloud/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
"cdk:test:destroy": "cdk destroy --app cdk.test.out",
"cdk:test:destroy:all": "cdk destroy --app cdk.test.out --all",
"cdk:test:clean": "rimraf cdk.test.out",
"cdk:dev:synth": "cdk synth -o cdk.dev.out --context STAGE=dev",
"cdk:dev:deploy": "cdk deploy --app cdk.dev.out --all",
"cdk:dev:destroy": "cdk destroy --app cdk.dev.out --all",
"cdk:dev:clean": "rimraf cdk.dev.out",
"codecheck": "concurrently \"npm run lint:check\" \"npm run format:check\"",
"format": "prettier . --write",
"format:check": "prettier . --check",
Expand Down
78 changes: 78 additions & 0 deletions cloud/permissions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# IAM Execution Policies

It's a bad idea to give AdministratorAccess to the CDK execution role, even with a permissions boundary in place.
Instead, we have custom policies authorizing all actions needed to deploy the stacks:

- `execution_policy_basics.json` Basic permissions for CDK deployments, including Lambda creation and execution
- `execution_policy_cloudfront.json` Permissions for creating a site Distribution with associated behaviors, cache
policies and origin forwarding policies
- `execution_policy_cognito.json` Permissions required for Cognito userpools, clients and domains
- `execution_policy_edgelambda.json` Permissions to create a Cloudfront Lambda@Edge function (in us-east-1)
- `execution_policy_route53.json` Permissions required for Route53 records and ACM certificates
- `execution_policy_vpc.json` Permissions for deploying, updating and destroying a load-balanced, Fargate-managed
container
- `execution_policy_pipeline.json` Permissions for deploying the pipelines, which then orchestrate all other stacks

## Commands:

### Upload IAM policies to your AWS environment

```shell
aws iam create-policy \
--policy-name cdk-execution-policy-basics \
--policy-document file://permissions/execution_policy_basics.json
--description "Baseline permissions for cloudformation deployments"

aws iam create-policy \
--policy-name cdk-execution-policy-pipeline \
--policy-document file://permissions/execution_policy_pipeline.json
--description "Permissions to deploy a codepipeline and codebuild projects"

aws iam create-policy \
--policy-name cdk-execution-policy-cloudfront \
--policy-document file://permissions/execution_policy_cloudfront.json
--description "Permissions to deploy cloudfront resources, except for lambda@edge functions"

aws iam create-policy \
--policy-name cdk-execution-policy-cognito \
--policy-document file://permissions/execution_policy_cognito.json
--description "Permissions to deploy cognito userpools and related resources"

aws iam create-policy \
--policy-name cdk-execution-policy-edgelambda \
--policy-document file://permissions/execution_policy_edgelambda.json
--description "Permissions to deploy lambda@edge functions for a cloudfront distribution"

aws iam create-policy \
--policy-name cdk-execution-policy-route53 \
--policy-document file://permissions/execution_policy_route53.json
--description "Permissions to deploy domain records and ACM certificates"

aws iam create-policy \
--policy-name cdk-execution-policy-vpc \
--policy-document file://permissions/execution_policy_vpc.json
--description "Permissions to deploy VPC, EC2 and ECS resources for a Fargate-managed container app"
```

### Bootstrap CDK using the uploaded execution policies

If your primary region is NOT `us-east-1`, then you need to bootstrap that region as well, because
CloudFront mandates some resources are deployed to `us-east-1`.

```shell
# Bootstrap primary region
npx cdk bootstrap aws://{account}/{region} \
--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"

# Bootstrap us-east-1 for cloudfront
npx cdk bootstrap aws://{account}/us-east-1 \
--cloudformation-execution-policies "arn:aws:iam::{account}:policy/cdk-execution-policy-basics,arn:aws:iam::{account}:policy/cdk-execution-policy-edgelambda"
```

If you ARE deploying your stage to us-east-1, then you only need one bootstrap command:

```shell
# Bootstrap us-east-1
npx cdk bootstrap aws://{account}/us-east-1 \
--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"
```
69 changes: 69 additions & 0 deletions cloud/permissions/execution_policy_basics.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "IAMRole",
"Effect": "Allow",
"Action": [
"iam:Get*",
"iam:List*",
"iam:CreateRole",
"iam:DeleteRole",
"iam:TagRole",
"iam:AttachRolePolicy",
"iam:DeleteRolePolicy",
"iam:DetachRolePolicy",
"iam:PutRolePolicy"
],
"Resource": "arn:aws:iam::*:role/*spylogic*"
},
{
"Sid": "IAMPassRole",
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": ["arn:aws:iam::*:role/*spylogic*"]
},
{
"Sid": "S3Read",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "*"
},
{
"Sid": "S3Write",
"Effect": "Allow",
"Action": [
"s3:CreateBucket",
"s3:DeleteBucket",
"s3:PutObject",
"s3:DeleteObject",
"s3:PutBucketPolicy",
"s3:DeleteBucketPolicy",
"s3:PutBucketTagging"
],
"Resource": "arn:aws:s3:::*spylogic*"
},
{
"Sid": "SSMRead",
"Effect": "Allow",
"Action": "ssm:GetParameters",
"Resource": "arn:aws:ssm:*:*:parameter/cdk-bootstrap/*/version"
},
{
"Sid": "Lambdas",
"Effect": "Allow",
"Action": [
"lambda:Get*",
"lambda:List*",
"lambda:CreateFunction",
"lambda:UpdateFunctionCode",
"lambda:UpdateFunctionConfiguration",
"lambda:DeleteFunction",
"lambda:InvokeFunction",
"lambda:TagResource",
"lambda:UntagResource"
],
"Resource": "arn:aws:lambda:*:*:function:*spylogic*"
}
]
}
Loading

0 comments on commit f19c9ab

Please sign in to comment.