Skip to content

Commit e42de43

Browse files
authored
feat: secure access to lambda functions (#2)
+ Change the SSR server and OPTIONS handler lambda function URLs to use AWS_IAM authorization. + Provide permission for the lamdba@egde function to sign requests to the lambda URLs. + Update tests (partially)
2 parents d3f7291 + 73cb111 commit e42de43

File tree

12 files changed

+156
-107
lines changed

12 files changed

+156
-107
lines changed

adapter.ts

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { writeFileSync } from 'fs'
22
import { join } from 'path'
33
import * as url from 'url'
44

5-
import { LocalProgramArgs, LocalWorkspace } from '@pulumi/pulumi/automation/index.js'
5+
import {
6+
LocalProgramArgs,
7+
LocalWorkspace,
8+
} from '@pulumi/pulumi/automation/index.js'
69
import {
710
buildServer,
811
buildOptions,
@@ -43,7 +46,7 @@ export function adapter({
4346
MEMORY_SIZE,
4447
zoneName = 'us-east-2',
4548
env = {},
46-
pulumiPaths = []
49+
pulumiPaths = [],
4750
}: AWSAdapterProps = {}) {
4851
/** @type {import('@sveltejs/kit').Adapter} */
4952
return {
@@ -69,12 +72,13 @@ export function adapter({
6972
serverArgs,
7073
{
7174
envVars: {
72-
'TS_NODE_IGNORE': '^(?!.*(sveltekit-adapter-aws-pulumi)).*'
73-
}
74-
})
75+
TS_NODE_IGNORE: '^(?!.*(sveltekit-adapter-aws-pulumi)).*',
76+
},
77+
}
78+
)
7579

7680
// Set the AWS region.
77-
await serverStack.setConfig("aws:region", { value: zoneName });
81+
await serverStack.setConfig('aws:region', { value: zoneName })
7882

7983
await serverStack.setAllConfig({
8084
projectPath: { value: '.env' },
@@ -83,7 +87,6 @@ export function adapter({
8387
memorySizeStr: { value: String(MEMORY_SIZE) },
8488
})
8589

86-
8790
const serverStackUpResult = await serverStack.up({
8891
onOutput: console.info,
8992
})
@@ -103,21 +106,21 @@ export function adapter({
103106
stackName: stackName,
104107
workDir: mainPath,
105108
}
106-
const mainStack = await LocalWorkspace.createOrSelectStack(
107-
mainArgs,
108-
{
109-
envVars: {
110-
'TS_NODE_IGNORE': '^(?!.*(sveltekit-adapter-aws-pulumi)).*'
111-
}
112-
})
109+
const mainStack = await LocalWorkspace.createOrSelectStack(mainArgs, {
110+
envVars: {
111+
TS_NODE_IGNORE: '^(?!.*(sveltekit-adapter-aws-pulumi)).*',
112+
},
113+
})
113114

114115
// Set the AWS region.
115-
await mainStack.setConfig("aws:region", { value: zoneName });
116+
await mainStack.setConfig('aws:region', { value: zoneName })
116117

117118
await mainStack.setAllConfig({
118119
edgePath: { value: edge_directory },
119120
staticPath: { value: static_directory },
120121
prerenderedPath: { value: prerendered_directory },
122+
serverArn: { value: serverStackUpResult.outputs.serverArn.value },
123+
optionsArn: { value: serverStackUpResult.outputs.optionsArn.value },
121124
})
122125

123126
if (FQDN) {
@@ -131,15 +134,17 @@ export function adapter({
131134
}
132135

133136
const mainStackUpResult = await mainStack.up({ onOutput: console.info })
134-
const mainAllowedOrigins = JSON.stringify(mainStackUpResult.outputs.allowedOrigins.value)
135-
137+
const mainAllowedOrigins = JSON.stringify(
138+
mainStackUpResult.outputs.allowedOrigins.value
139+
)
140+
136141
let serverAllowedOrigins: string = ''
137142
const serverConfig = await serverStack.getAllConfig()
138-
139-
if ('allowedOrigins' in serverConfig){
143+
144+
if ('allowedOrigins' in serverConfig) {
140145
serverAllowedOrigins = serverConfig['allowedOrigins'].value
141146
}
142-
147+
143148
if (serverAllowedOrigins !== mainAllowedOrigins) {
144149
// Call the server stack setting the allowed origins
145150
await serverStack.setConfig('allowedOrigins', {

bin/destroy.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,8 @@ export async function main(args: string[]): Promise<void> {
3131
throw error
3232
}
3333
}
34-
34+
3535
for (const pulumiPath of adapterProps.pulumiPaths!) {
36-
3736
spawnSync(
3837
'pulumi',
3938
['destroy', '-f', '-s', adapterProps.stackName!, '-y', '--refresh'],
@@ -43,7 +42,6 @@ export async function main(args: string[]): Promise<void> {
4342
env: process.env,
4443
}
4544
)
46-
4745
}
4846
}
4947

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@
5757
"@pulumi/command": "^0.7.1",
5858
"@pulumi/pulumi": "^3.0.0",
5959
"dotenv": "^16.0.3",
60-
"minimist": "^1.2.8",
61-
"sveltekit-adapter-aws-base": "~1.4.8",
6260
"folder-hash": "^4.0.4",
63-
"lodash": "^4.17.21"
61+
"lodash": "^4.17.21",
62+
"minimist": "^1.2.8",
63+
"sveltekit-adapter-aws-base": "^1.5.5"
6464
},
6565
"release": {
6666
"branches": [

stacks/main/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ const staticPath = pulumiConfig.get('staticPath')
1616
const prerenderedPath = pulumiConfig.get('prerenderedPath')
1717
const FQDN = pulumiConfig.get('FQDN')
1818
const serverHeadersStr = pulumiConfig.get('serverHeaders')
19+
const serverArn = pulumiConfig.get('serverArn')
20+
const optionsArn = pulumiConfig.get('optionsArn')
1921

2022
const [_, zoneName, ...MLDs] = FQDN!.split('.') || []
2123
const domainName = [zoneName, ...MLDs].join('.')
@@ -26,12 +28,11 @@ if (serverHeadersStr) {
2628
serverHeaders = JSON.parse(serverHeadersStr)
2729
}
2830

29-
const iamForLambda = getLambdaRole()
31+
const iamForLambda = getLambdaRole([serverArn!, optionsArn!])
3032
const routerHandler = buildRouter(iamForLambda, edgePath!)
3133

3234
let certificateArn: pulumi.Input<string> | undefined
3335

34-
3536
if (FQDN) {
3637
certificateArn = validateCertificate(FQDN!, domainName)
3738
}

stacks/main/resources.ts

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,35 +17,74 @@ const eastRegion = new aws.Provider(registerName('ProviderEast'), {
1717
region: 'us-east-1',
1818
})
1919

20-
export function getLambdaRole(): aws.iam.Role {
20+
export function getLambdaRole(functionArns?: string[]): aws.iam.Role {
21+
interface IAMPolicy {
22+
statements: [
23+
{
24+
principals?: [
25+
{
26+
type: string
27+
identifiers: string[]
28+
}
29+
]
30+
actions: string[]
31+
effect: string
32+
resources?: string[]
33+
}
34+
]
35+
}
36+
37+
let lambdaPolicyStub: IAMPolicy = {
38+
statements: [
39+
{
40+
principals: [
41+
{
42+
type: 'Service',
43+
identifiers: ['lambda.amazonaws.com', 'edgelambda.amazonaws.com'],
44+
},
45+
],
46+
actions: ['sts:AssumeRole'],
47+
effect: 'Allow',
48+
},
49+
],
50+
}
51+
52+
let lambdaPolicyDocument = aws.iam.getPolicyDocumentOutput(lambdaPolicyStub)
2153
const iamForLambda = new aws.iam.Role(registerName('IamForLambda'), {
22-
assumeRolePolicy: `{
23-
"Version": "2012-10-17",
24-
"Statement": [
25-
{
26-
"Action": "sts:AssumeRole",
27-
"Principal": {
28-
"Service": [
29-
"lambda.amazonaws.com",
30-
"edgelambda.amazonaws.com"
31-
]
32-
},
33-
"Effect": "Allow",
34-
"Sid": ""
35-
}
36-
]
37-
}
38-
`,
54+
assumeRolePolicy: lambdaPolicyDocument.json,
3955
})
4056

41-
const RPA = new aws.iam.RolePolicyAttachment(
57+
new aws.iam.RolePolicyAttachment(
4258
registerName('ServerRPABasicExecutionRole'),
4359
{
4460
role: iamForLambda.name,
4561
policyArn: aws.iam.ManagedPolicy.AWSLambdaBasicExecutionRole,
4662
}
4763
)
4864

65+
if (functionArns) {
66+
lambdaPolicyStub = {
67+
statements: [
68+
{
69+
actions: ['lambda:InvokeFunctionUrl'],
70+
effect: 'Allow',
71+
resources: functionArns,
72+
},
73+
],
74+
}
75+
76+
lambdaPolicyDocument = aws.iam.getPolicyDocumentOutput(lambdaPolicyStub)
77+
78+
const policy = new aws.iam.Policy(registerName('invokePolicy'), {
79+
policy: lambdaPolicyDocument.json,
80+
})
81+
82+
new aws.iam.RolePolicyAttachment(registerName('ServerRPAInvokePolicy'), {
83+
role: iamForLambda.name,
84+
policyArn: policy.arn,
85+
})
86+
}
87+
4988
return iamForLambda
5089
}
5190

@@ -197,7 +236,7 @@ export function buildCDN(
197236
description: 'Default Origin Access Control',
198237
name: 'CloudFrontOriginAccessControl',
199238
originAccessControlOriginType: 's3',
200-
signingBehavior: 'always',
239+
signingBehavior: 'no-override',
201240
signingProtocol: 'sigv4',
202241
}
203242
)
@@ -206,9 +245,6 @@ export function buildCDN(
206245
name: 'Managed-CachingOptimized',
207246
})
208247

209-
// serverFunctionURL.functionUrl.apply(
210-
// (endpoint) => endpoint.split('://')[1].slice(0, -1)
211-
212248
const distribution = new aws.cloudfront.Distribution(
213249
registerName('CloudFrontDistribution'),
214250
{
@@ -252,6 +288,7 @@ export function buildCDN(
252288
.apply(([arn, version]) => {
253289
return `${arn}:${version}`
254290
}),
291+
includeBody: true,
255292
},
256293
],
257294
originRequestPolicyId: defaultRequestPolicy.id,

stacks/server/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@ const optionsURL = buildLambda(
4040
optionsEnv
4141
)
4242

43+
export const serverArn = serverURL.functionArn
4344
export const serverDomain = serverURL.functionUrl.apply((endpoint) =>
4445
endpoint.split('://')[1].slice(0, -1)
4546
)
4647

48+
export const optionsArn = optionsURL.functionArn
4749
export const optionsDomain = optionsURL.functionUrl.apply((endpoint) =>
4850
endpoint.split('://')[1].slice(0, -1)
4951
)

stacks/server/resources.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function buildLambda(
6363

6464
const lambdaURL = new aws.lambda.FunctionUrl(`${name}URL`, {
6565
functionName: lambdaHandler.arn,
66-
authorizationType: 'NONE',
66+
authorizationType: 'AWS_IAM',
6767
})
6868

6969
return lambdaURL

tests/adapter.test.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,37 @@ vi.mock('@pulumi/pulumi/automation/index.js', () => {
1414
const Stack = {
1515
setConfig: vi.fn(),
1616
setAllConfig: vi.fn(),
17-
getAllConfig: vi.fn(() => {return {}}),
18-
up: vi.fn(() => {return {
19-
outputs: {
20-
serverDomain: {
21-
value: 'mock'
22-
},
23-
optionsDomain: {
24-
value: 'mock'
17+
getAllConfig: vi.fn(() => {
18+
return {}
19+
}),
20+
up: vi.fn(() => {
21+
return {
22+
outputs: {
23+
serverArn: {
24+
value: 'mock',
25+
},
26+
optionsArn: {
27+
value: 'mock',
28+
},
29+
serverDomain: {
30+
value: 'mock',
31+
},
32+
optionsDomain: {
33+
value: 'mock',
34+
},
35+
allowedOrigins: {
36+
value: ['mock'],
37+
},
2538
},
26-
allowedOrigins: {
27-
value: ['mock']
28-
}
2939
}
30-
}
31-
}),
40+
}),
3241
}
3342
const LocalWorkspace = {
34-
createOrSelectStack: vi.fn(() => Stack)
43+
createOrSelectStack: vi.fn(() => Stack),
3544
}
36-
45+
3746
return {
38-
LocalWorkspace
47+
LocalWorkspace,
3948
}
4049
})
4150

@@ -57,10 +66,10 @@ describe('adapter.ts', () => {
5766
})
5867
;(buildOptions as any).mockImplementation(() => {
5968
return 'mock'
60-
})
69+
})
6170
;(buildRouter as any).mockImplementation(() => {
6271
return 'mock'
63-
})
72+
})
6473

6574
const builder = {
6675
log: {

tests/bin.destroy.test.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,7 @@ describe('bin/destroy.ts', () => {
3333
const propsPath = path.join(buildDir, '.adapterprops.json')
3434

3535
const expectedStackName = 'mock'
36-
const expectedPulumiPaths = [
37-
'stacks/one',
38-
'stacks/two'
39-
]
36+
const expectedPulumiPaths = ['stacks/one', 'stacks/two']
4037
const json = JSON.stringify({
4138
stackName: expectedStackName,
4239
pulumiPaths: expectedPulumiPaths,
@@ -71,10 +68,7 @@ describe('bin/destroy.ts', () => {
7168
const propsPath = path.join(tmpDir, '.adapterprops.json')
7269

7370
const expectedStackName = 'mock'
74-
const expectedPulumiPaths = [
75-
'stacks/one',
76-
'stacks/two'
77-
]
71+
const expectedPulumiPaths = ['stacks/one', 'stacks/two']
7872
const json = JSON.stringify({
7973
stackName: expectedStackName,
8074
pulumiPaths: expectedPulumiPaths,

0 commit comments

Comments
 (0)