@deployfoundation/foundation-deploy 0.2.52 → 0.2.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/app.js +2 -2
- package/dist/bin/foundation-deploy.js +8 -6
- package/dist/{chunk-sgdcmme0.js → chunk-1pww63wd.js} +2 -2
- package/dist/{chunk-rxetj2p9.js → chunk-fjdgxw8h.js} +52 -2
- package/dist/{chunk-9zx95xs6.js → chunk-nvsj7bbw.js} +1 -1
- package/dist/{chunk-59dpqyap.js → chunk-tfn9tvaa.js} +87 -1
- package/dist/src/index.js +2 -2
- package/package.json +1 -1
- package/src/deploy/runtime-secret.ts +9 -0
- package/src/stacks/agent-stack.ts +34 -0
- package/src/stacks/network-stack.ts +15 -0
- package/src/stacks/secret-bearing-reads.ts +131 -0
package/dist/bin/app.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
FoundationPages,
|
|
9
9
|
FoundationPipeline,
|
|
10
10
|
NewsletterStack
|
|
11
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-tfn9tvaa.js";
|
|
12
12
|
import"../chunk-pxhftba1.js";
|
|
13
13
|
import"../chunk-dz91avrb.js";
|
|
14
14
|
import {
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
instanceNames,
|
|
17
17
|
loadInstanceFile,
|
|
18
18
|
provisionsIntegration
|
|
19
|
-
} from "../chunk-
|
|
19
|
+
} from "../chunk-fjdgxw8h.js";
|
|
20
20
|
import"../chunk-96fjaysn.js";
|
|
21
21
|
import"../chunk-5wgwscd0.js";
|
|
22
22
|
|
|
@@ -32,13 +32,14 @@ import {
|
|
|
32
32
|
secretExists,
|
|
33
33
|
stackExists,
|
|
34
34
|
stackOutput
|
|
35
|
-
} from "../chunk-
|
|
35
|
+
} from "../chunk-nvsj7bbw.js";
|
|
36
36
|
import {
|
|
37
37
|
CUSTOMIZATION_ARTIFACT_NAME,
|
|
38
38
|
instanceNames,
|
|
39
|
+
mongoCollectionPolicyEnv,
|
|
39
40
|
slackCommandPrefix,
|
|
40
41
|
versionedConfigKey
|
|
41
|
-
} from "../chunk-
|
|
42
|
+
} from "../chunk-fjdgxw8h.js";
|
|
42
43
|
import"../chunk-m76jt5rc.js";
|
|
43
44
|
import"../chunk-3614v9rr.js";
|
|
44
45
|
import {
|
|
@@ -1216,6 +1217,7 @@ function composeRuntimeSecret(inputs) {
|
|
|
1216
1217
|
...inputs.etradeProxyFunctionArn !== undefined && inputs.etradeProxyFunctionArn !== "" ? { FOUNDATION_ETRADE_PROXY_ARN: inputs.etradeProxyFunctionArn } : {},
|
|
1217
1218
|
...inputs.routineIngressFunctionArn !== undefined && inputs.routineIngressFunctionArn !== "" ? { FOUNDATION_ROUTINE_INGRESS_FUNCTION_ARN: inputs.routineIngressFunctionArn } : {},
|
|
1218
1219
|
FOUNDATION_MONGODB_SECRET_ID: names.secretMongodbReadonly,
|
|
1220
|
+
...instance.mongodbReadonly.deniedCollections.length > 0 || instance.mongodbReadonly.unsafeReadDefaultDeniedCollections.length > 0 ? { FOUNDATION_MONGODB_COLLECTION_POLICY: mongoCollectionPolicyEnv(instance.mongodbReadonly) } : {},
|
|
1219
1221
|
FOUNDATION_SLACK_TEAM_ID: slackApp.team_id !== undefined && slackApp.team_id !== "" ? slackApp.team_id : instance.slack.teamId
|
|
1220
1222
|
};
|
|
1221
1223
|
}
|
|
@@ -2538,7 +2540,7 @@ ${USAGE}`);
|
|
|
2538
2540
|
const tenantId = flag2(args, "--tenant");
|
|
2539
2541
|
if (baseUrl === undefined || tenantId === undefined || !args.includes("--key-stdin"))
|
|
2540
2542
|
throw new Error("crm-check --base-url needs --tenant <id> and --key-stdin");
|
|
2541
|
-
const { crmCheckTarget, defaultCrmCommandDeps } = await import("../chunk-
|
|
2543
|
+
const { crmCheckTarget, defaultCrmCommandDeps } = await import("../chunk-1pww63wd.js");
|
|
2542
2544
|
return crmCheckTarget({
|
|
2543
2545
|
baseUrl,
|
|
2544
2546
|
tenantId,
|
|
@@ -2611,21 +2613,21 @@ ${USAGE}`);
|
|
|
2611
2613
|
return 0;
|
|
2612
2614
|
}
|
|
2613
2615
|
case "crm-key": {
|
|
2614
|
-
const { crmKey, defaultCrmCommandDeps } = await import("../chunk-
|
|
2616
|
+
const { crmKey, defaultCrmCommandDeps } = await import("../chunk-1pww63wd.js");
|
|
2615
2617
|
await crmKey(ctx, { stdin: args.includes("--stdin") }, defaultCrmCommandDeps());
|
|
2616
2618
|
return 0;
|
|
2617
2619
|
}
|
|
2618
2620
|
case "crm-check": {
|
|
2619
2621
|
if (args.includes("--write"))
|
|
2620
2622
|
throw new Error("crm-check --write creates records and spends the week's outreach allowance, so it never runs against the instance's own tenant: use --base-url with a sandbox tenant");
|
|
2621
|
-
const { crmCheck, defaultCrmCommandDeps } = await import("../chunk-
|
|
2623
|
+
const { crmCheck, defaultCrmCommandDeps } = await import("../chunk-1pww63wd.js");
|
|
2622
2624
|
return crmCheck(ctx, { allowSkips: args.includes("--allow-skips") }, defaultCrmCommandDeps());
|
|
2623
2625
|
}
|
|
2624
2626
|
case "crm-replay": {
|
|
2625
2627
|
const from = flag2(args, "--from");
|
|
2626
2628
|
if (from === undefined || from.startsWith("-"))
|
|
2627
2629
|
throw new Error("crm-replay needs --from <file>: the writes to replay, as JSONL");
|
|
2628
|
-
const { crmReplay, defaultCrmCommandDeps } = await import("../chunk-
|
|
2630
|
+
const { crmReplay, defaultCrmCommandDeps } = await import("../chunk-1pww63wd.js");
|
|
2629
2631
|
const result = await crmReplay(ctx, { from }, defaultCrmCommandDeps());
|
|
2630
2632
|
return result.refused.length > 0 ? 1 : 0;
|
|
2631
2633
|
}
|
|
@@ -7,6 +7,34 @@ import {
|
|
|
7
7
|
// ../core/src/instance.ts
|
|
8
8
|
import { parse as parseYaml } from "yaml";
|
|
9
9
|
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
// ../core/src/mongodb-collections.ts
|
|
12
|
+
var DEFAULT_DENIED_MONGO_COLLECTIONS = [
|
|
13
|
+
"users",
|
|
14
|
+
"sessions",
|
|
15
|
+
"refreshtokens",
|
|
16
|
+
"passwordresets",
|
|
17
|
+
"servicekeys",
|
|
18
|
+
"machineapirequests",
|
|
19
|
+
"apikeys",
|
|
20
|
+
"oauthtokens",
|
|
21
|
+
"integrationtokens",
|
|
22
|
+
"integrations"
|
|
23
|
+
];
|
|
24
|
+
var DENIED_MONGO_COLLECTION_PATTERN = /token|secret|session|credential|password/i;
|
|
25
|
+
function deniedByDefault(name) {
|
|
26
|
+
const lower = name.toLowerCase();
|
|
27
|
+
return DEFAULT_DENIED_MONGO_COLLECTIONS.includes(lower) || DENIED_MONGO_COLLECTION_PATTERN.test(lower);
|
|
28
|
+
}
|
|
29
|
+
function mongoCollectionPolicyEnv(section) {
|
|
30
|
+
const policy = {
|
|
31
|
+
denied: section.deniedCollections.map((name) => name.toLowerCase()),
|
|
32
|
+
unsafeAllowed: section.unsafeReadDefaultDeniedCollections.map((name) => name.toLowerCase())
|
|
33
|
+
};
|
|
34
|
+
return JSON.stringify(policy);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ../core/src/instance.ts
|
|
10
38
|
var PENDING = "TBD";
|
|
11
39
|
var CUSTOMIZATION_ARTIFACT_NAME = "Customization";
|
|
12
40
|
var RawAws = z.object({
|
|
@@ -234,6 +262,27 @@ var RawCrmProvider = z.discriminatedUnion("kind", [
|
|
|
234
262
|
}).strict()
|
|
235
263
|
]);
|
|
236
264
|
var RawCrm = z.object({ provider: RawCrmProvider.default({ kind: "dynamodb" }) }).strict().default({});
|
|
265
|
+
var MongoCollectionName = z.string().regex(/^[A-Za-z0-9_.$-]{1,120}$/, "a MongoDB collection name, e.g. auditlogs");
|
|
266
|
+
var RawMongodbReadonly = z.object({
|
|
267
|
+
deniedCollections: z.array(MongoCollectionName).max(200).default([]),
|
|
268
|
+
unsafeReadDefaultDeniedCollections: z.array(MongoCollectionName).max(50).default([])
|
|
269
|
+
}).strict().superRefine((section, ctx) => {
|
|
270
|
+
const denied = new Set(section.deniedCollections.map((name) => name.toLowerCase()));
|
|
271
|
+
section.unsafeReadDefaultDeniedCollections.forEach((name, index) => {
|
|
272
|
+
if (denied.has(name.toLowerCase()))
|
|
273
|
+
ctx.addIssue({
|
|
274
|
+
code: z.ZodIssueCode.custom,
|
|
275
|
+
path: ["unsafeReadDefaultDeniedCollections", index],
|
|
276
|
+
message: `${name} is in deniedCollections too; remove it from one list`
|
|
277
|
+
});
|
|
278
|
+
else if (!deniedByDefault(name))
|
|
279
|
+
ctx.addIssue({
|
|
280
|
+
code: z.ZodIssueCode.custom,
|
|
281
|
+
path: ["unsafeReadDefaultDeniedCollections", index],
|
|
282
|
+
message: `${name} is not denied by default, so there is nothing to opt out of`
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
}).default({});
|
|
237
286
|
var RawInstance = z.object({
|
|
238
287
|
name: z.string().min(1).max(63).regex(/^[a-z][a-z0-9-]*$/),
|
|
239
288
|
displayName: z.string().min(1).max(120),
|
|
@@ -252,7 +301,8 @@ var RawInstance = z.object({
|
|
|
252
301
|
integrations: RawIntegrations,
|
|
253
302
|
newsletter: RawNewsletter,
|
|
254
303
|
pages: RawPages,
|
|
255
|
-
crm: RawCrm
|
|
304
|
+
crm: RawCrm,
|
|
305
|
+
mongodbReadonly: RawMongodbReadonly
|
|
256
306
|
}).strict().superRefine((doc, ctx) => {
|
|
257
307
|
if (doc.crm.provider.kind === "http" && !doc.integrations.crm)
|
|
258
308
|
ctx.addIssue({
|
|
@@ -463,4 +513,4 @@ var WEB_SEARCH_TARGET = "websearch";
|
|
|
463
513
|
var WEB_SEARCH_CONNECTOR_VERSION = "1.2.0";
|
|
464
514
|
var LIVE_ENDPOINT_NAME = "live";
|
|
465
515
|
|
|
466
|
-
export { CUSTOMIZATION_ARTIFACT_NAME, newsletterManifestFor, FOUNDATION_VERSION_RE, versionedConfigKey, instanceNames, slackCommandPrefix, slackCommandPrefixes, loadInstanceFile, configPathFor, capabilityEnabled, requiresAuthenticatedQueue, enabledIntegrations, provisionsIntegration, adminsFor, WEB_SEARCH_TARGET, WEB_SEARCH_CONNECTOR_VERSION, LIVE_ENDPOINT_NAME };
|
|
516
|
+
export { mongoCollectionPolicyEnv, CUSTOMIZATION_ARTIFACT_NAME, newsletterManifestFor, FOUNDATION_VERSION_RE, versionedConfigKey, instanceNames, slackCommandPrefix, slackCommandPrefixes, loadInstanceFile, configPathFor, capabilityEnabled, requiresAuthenticatedQueue, enabledIntegrations, provisionsIntegration, adminsFor, WEB_SEARCH_TARGET, WEB_SEARCH_CONNECTOR_VERSION, LIVE_ENDPOINT_NAME };
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
provisionsIntegration,
|
|
18
18
|
requiresAuthenticatedQueue,
|
|
19
19
|
slackCommandPrefixes
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-fjdgxw8h.js";
|
|
21
21
|
import {
|
|
22
22
|
capabilityScheduledJobs,
|
|
23
23
|
crmOutreachConfigured,
|
|
@@ -35,6 +35,70 @@ import * as cdk from "aws-cdk-lib";
|
|
|
35
35
|
import * as ecr from "aws-cdk-lib/aws-ecr";
|
|
36
36
|
import * as iam from "aws-cdk-lib/aws-iam";
|
|
37
37
|
import * as logs from "aws-cdk-lib/aws-logs";
|
|
38
|
+
|
|
39
|
+
// src/stacks/secret-bearing-reads.ts
|
|
40
|
+
var SECRET_BEARING_READ_ACTIONS = [
|
|
41
|
+
"wafv2:GetSampledRequests",
|
|
42
|
+
"waf:GetSampledRequests",
|
|
43
|
+
"waf-regional:GetSampledRequests",
|
|
44
|
+
"lambda:GetFunction",
|
|
45
|
+
"lambda:GetFunctionConfiguration",
|
|
46
|
+
"lambda:ListFunctions",
|
|
47
|
+
"lambda:ListVersionsByFunction",
|
|
48
|
+
"lambda:GetLayerVersion",
|
|
49
|
+
"lambda:GetLayerVersionByArn",
|
|
50
|
+
"ecs:DescribeTaskDefinition",
|
|
51
|
+
"apprunner:DescribeService",
|
|
52
|
+
"batch:DescribeJobDefinitions",
|
|
53
|
+
"batch:DescribeJobs",
|
|
54
|
+
"codebuild:BatchGetProjects",
|
|
55
|
+
"codebuild:BatchGetBuilds",
|
|
56
|
+
"codebuild:BatchGetBuildBatches",
|
|
57
|
+
"amplify:GetApp",
|
|
58
|
+
"amplify:ListApps",
|
|
59
|
+
"amplify:GetBranch",
|
|
60
|
+
"amplify:ListBranches",
|
|
61
|
+
"amplify:GetBackendEnvironment",
|
|
62
|
+
"elasticbeanstalk:DescribeConfigurationSettings",
|
|
63
|
+
"ec2:DescribeLaunchTemplateVersions",
|
|
64
|
+
"ec2:DescribeInstanceAttribute",
|
|
65
|
+
"ssm:GetParameter",
|
|
66
|
+
"ssm:GetParameters",
|
|
67
|
+
"ssm:GetParametersByPath",
|
|
68
|
+
"ssm:GetParameterHistory",
|
|
69
|
+
"cloudformation:GetTemplate",
|
|
70
|
+
"appconfig:GetConfiguration",
|
|
71
|
+
"appconfig:GetHostedConfigurationVersion",
|
|
72
|
+
"glue:GetJob",
|
|
73
|
+
"glue:GetJobs",
|
|
74
|
+
"glue:BatchGetJobs",
|
|
75
|
+
"glue:GetConnection",
|
|
76
|
+
"glue:GetConnections",
|
|
77
|
+
"glue:GetDevEndpoint",
|
|
78
|
+
"glue:GetDevEndpoints",
|
|
79
|
+
"glue:BatchGetDevEndpoints",
|
|
80
|
+
"sagemaker:DescribeModel",
|
|
81
|
+
"sagemaker:DescribeTrainingJob",
|
|
82
|
+
"sagemaker:DescribeProcessingJob",
|
|
83
|
+
"sagemaker:DescribeTransformJob",
|
|
84
|
+
"datapipeline:GetPipelineDefinition",
|
|
85
|
+
"appsync:ListApiKeys",
|
|
86
|
+
"appsync:GetDataSource",
|
|
87
|
+
"appsync:ListDataSources",
|
|
88
|
+
"kinesisanalytics:DescribeApplication",
|
|
89
|
+
"lightsail:GetBucketAccessKeys",
|
|
90
|
+
"lightsail:GetContainerServices",
|
|
91
|
+
"lightsail:GetContainerServiceDeployments",
|
|
92
|
+
"opsworks:DescribeApps",
|
|
93
|
+
"opsworks:DescribeStacks",
|
|
94
|
+
"cognito-identity:GetCredentialsForIdentity",
|
|
95
|
+
"cognito-identity:GetOpenIdToken",
|
|
96
|
+
"cognito-identity:GetOpenIdTokenForDeveloperIdentity",
|
|
97
|
+
"connect:GetFederationToken",
|
|
98
|
+
"codeartifact:GetAuthorizationToken"
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
// src/stacks/agent-stack.ts
|
|
38
102
|
class FoundationAgent extends cdk.Stack {
|
|
39
103
|
repository;
|
|
40
104
|
executionRole;
|
|
@@ -77,6 +141,7 @@ class FoundationAgent extends cdk.Stack {
|
|
|
77
141
|
this.accessPoint = accessPoint;
|
|
78
142
|
this.executionRole = this.buildExecutionRole(props);
|
|
79
143
|
this.readOnlyRole = this.buildReadOnlyRole(props);
|
|
144
|
+
this.secretBearingReadsPolicy(props.instance, [this.executionRole, this.readOnlyRole]);
|
|
80
145
|
this.buildSpansIngestionPolicy();
|
|
81
146
|
const gateway = this.buildToolGateway(display);
|
|
82
147
|
this.webSearchUrl = gateway.getAtt("GatewayUrl").toString();
|
|
@@ -439,6 +504,20 @@ class FoundationAgent extends cdk.Stack {
|
|
|
439
504
|
}));
|
|
440
505
|
return role;
|
|
441
506
|
}
|
|
507
|
+
secretBearingReadsPolicy(instance, roles) {
|
|
508
|
+
new iam.ManagedPolicy(this, "AgentSecretBearingReadsDeny", {
|
|
509
|
+
description: `${instance.displayName} agent: no AWS read whose response carries a secret`,
|
|
510
|
+
roles,
|
|
511
|
+
statements: [
|
|
512
|
+
new iam.PolicyStatement({
|
|
513
|
+
sid: "DenySecretBearingReads",
|
|
514
|
+
effect: iam.Effect.DENY,
|
|
515
|
+
actions: [...SECRET_BEARING_READ_ACTIONS],
|
|
516
|
+
resources: ["*"]
|
|
517
|
+
})
|
|
518
|
+
]
|
|
519
|
+
});
|
|
520
|
+
}
|
|
442
521
|
buildSpansIngestionPolicy() {
|
|
443
522
|
return new logs.ResourcePolicy(this, "AgentSpansIngestion", {
|
|
444
523
|
resourcePolicyName: `${this.stackName}-AgentSpansFromXray`,
|
|
@@ -2657,6 +2736,13 @@ class FoundationNetwork extends cdk5.Stack {
|
|
|
2657
2736
|
actions: ["s3:GetObject"],
|
|
2658
2737
|
resources: [`arn:${this.partition}:s3:::prod-${this.region}-starport-layer-bucket/*`]
|
|
2659
2738
|
}));
|
|
2739
|
+
s3Endpoint.addToPolicy(new iam6.PolicyStatement({
|
|
2740
|
+
sid: "AmplifyArtifactsBucket",
|
|
2741
|
+
effect: iam6.Effect.ALLOW,
|
|
2742
|
+
principals: [new iam6.AnyPrincipal],
|
|
2743
|
+
actions: ["s3:GetObject"],
|
|
2744
|
+
resources: [`arn:${this.partition}:s3:::aws-amplify-prod-${this.region}-artifacts/*`]
|
|
2745
|
+
}));
|
|
2660
2746
|
}
|
|
2661
2747
|
if (!skipEndpoints.has("dynamodb")) {
|
|
2662
2748
|
const dynamoEndpoint = this.vpc.addGatewayEndpoint("DynamoDbEndpoint", {
|
package/dist/src/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
FoundationPipeline,
|
|
11
11
|
NewsletterStack,
|
|
12
12
|
deployStatements
|
|
13
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-tfn9tvaa.js";
|
|
14
14
|
import {
|
|
15
15
|
BUN_VERSION,
|
|
16
16
|
LAMBDA_ENTRY_POINTS,
|
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
loadInstanceFile,
|
|
48
48
|
provisionsIntegration,
|
|
49
49
|
requiresAuthenticatedQueue
|
|
50
|
-
} from "../chunk-
|
|
50
|
+
} from "../chunk-fjdgxw8h.js";
|
|
51
51
|
import"../chunk-96fjaysn.js";
|
|
52
52
|
import"../chunk-5wgwscd0.js";
|
|
53
53
|
export {
|
package/package.json
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
instanceNames as namesFor,
|
|
13
13
|
slackCommandPrefix,
|
|
14
14
|
} from "@deployfoundation/foundation-core/instance";
|
|
15
|
+
import { mongoCollectionPolicyEnv } from "@deployfoundation/foundation-core/mongodb-collections";
|
|
15
16
|
|
|
16
17
|
/** The pinned AgentCore endpoint a deploy promotes and the invoker calls. */
|
|
17
18
|
export const LIVE_ENDPOINT = "live";
|
|
@@ -258,6 +259,14 @@ export function composeRuntimeSecret(inputs: RuntimeSecretInputs): Record<string
|
|
|
258
259
|
// LAZILY, so a secret still holding the CDK placeholder costs nothing
|
|
259
260
|
// until someone asks a MongoDB question.
|
|
260
261
|
FOUNDATION_MONGODB_SECRET_ID: names.secretMongodbReadonly,
|
|
262
|
+
// Collections the MongoDB tools refuse beyond the compiled-in defaults,
|
|
263
|
+
// from the instance file and never from config.yaml, so only a reviewed
|
|
264
|
+
// deploy changes it. Absent when the instance file adds nothing: the
|
|
265
|
+
// defaults hold without it.
|
|
266
|
+
...(instance.mongodbReadonly.deniedCollections.length > 0 ||
|
|
267
|
+
instance.mongodbReadonly.unsafeReadDefaultDeniedCollections.length > 0
|
|
268
|
+
? { FOUNDATION_MONGODB_COLLECTION_POLICY: mongoCollectionPolicyEnv(instance.mongodbReadonly) }
|
|
269
|
+
: {}),
|
|
261
270
|
// CRM storage, the Otter credential, the Knock OAuth secrets, and the
|
|
262
271
|
// Upwork/E*TRADE OAuth secrets are intentionally absent here: the
|
|
263
272
|
// shell-capable agent receives only their fixed proxy ARNs.
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
provisionsIntegration,
|
|
24
24
|
requiresAuthenticatedQueue,
|
|
25
25
|
} from "../names.ts";
|
|
26
|
+
import { SECRET_BEARING_READ_ACTIONS } from "./secret-bearing-reads.ts";
|
|
26
27
|
|
|
27
28
|
export interface FoundationAgentProps extends cdk.StackProps {
|
|
28
29
|
/** The deployment this stack belongs to; every name below comes from it. */
|
|
@@ -154,6 +155,7 @@ export class FoundationAgent extends cdk.Stack {
|
|
|
154
155
|
this.accessPoint = accessPoint;
|
|
155
156
|
this.executionRole = this.buildExecutionRole(props);
|
|
156
157
|
this.readOnlyRole = this.buildReadOnlyRole(props);
|
|
158
|
+
this.secretBearingReadsPolicy(props.instance, [this.executionRole, this.readOnlyRole]);
|
|
157
159
|
this.buildSpansIngestionPolicy();
|
|
158
160
|
|
|
159
161
|
// The gateway is independent of the runtime: it can (and does) exist
|
|
@@ -637,6 +639,8 @@ export class FoundationAgent extends cdk.Stack {
|
|
|
637
639
|
* `ReadOnlyAccess` is AWS's own managed read-only policy. The inline
|
|
638
640
|
* additions are the two things it does NOT cover which the observability
|
|
639
641
|
* tools need: starting a Logs Insights query, and reading trace summaries.
|
|
642
|
+
* What it covers but must not, the reads whose responses carry secrets,
|
|
643
|
+
* is denied by `secretBearingReadsPolicy`.
|
|
640
644
|
*/
|
|
641
645
|
private buildReadOnlyRole(props: FoundationAgentProps): iam.Role {
|
|
642
646
|
const role = new iam.Role(this, "AgentReadOnlyRole", {
|
|
@@ -709,6 +713,36 @@ export class FoundationAgent extends cdk.Stack {
|
|
|
709
713
|
return role;
|
|
710
714
|
}
|
|
711
715
|
|
|
716
|
+
/**
|
|
717
|
+
* `ReadOnlyAccess` reads that return secrets: Lambda and container
|
|
718
|
+
* environment variables, user data, job and connection arguments, sampled
|
|
719
|
+
* WAF request headers, minted tokens ({@link SECRET_BEARING_READ_ACTIONS}).
|
|
720
|
+
*
|
|
721
|
+
* Both shell-reachable roles carry the deny. The read-only role needs it
|
|
722
|
+
* today, since its managed policy grants every one of these. The execution
|
|
723
|
+
* role grants none of them, and it gets the deny anyway for the same reason
|
|
724
|
+
* as the secret denies above: no future broad grant may reopen them.
|
|
725
|
+
*
|
|
726
|
+
* A managed policy of its own, like the pages policy, because the execution
|
|
727
|
+
* role's inline policy is near IAM's size limit. `Resource: "*"` because
|
|
728
|
+
* the point is every function, project and parameter in the account, not
|
|
729
|
+
* the instance's own.
|
|
730
|
+
*/
|
|
731
|
+
private secretBearingReadsPolicy(instance: Instance, roles: iam.IRole[]): void {
|
|
732
|
+
new iam.ManagedPolicy(this, "AgentSecretBearingReadsDeny", {
|
|
733
|
+
description: `${instance.displayName} agent: no AWS read whose response carries a secret`,
|
|
734
|
+
roles,
|
|
735
|
+
statements: [
|
|
736
|
+
new iam.PolicyStatement({
|
|
737
|
+
sid: "DenySecretBearingReads",
|
|
738
|
+
effect: iam.Effect.DENY,
|
|
739
|
+
actions: [...SECRET_BEARING_READ_ACTIONS],
|
|
740
|
+
resources: ["*"],
|
|
741
|
+
}),
|
|
742
|
+
],
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
|
|
712
746
|
/**
|
|
713
747
|
* Let X-Ray write this runtime's spans into the runtime's own log group.
|
|
714
748
|
*
|
|
@@ -247,6 +247,21 @@ export class FoundationNetwork extends cdk.Stack {
|
|
|
247
247
|
resources: [`arn:${this.partition}:s3:::prod-${this.region}-starport-layer-bucket/*`],
|
|
248
248
|
}),
|
|
249
249
|
);
|
|
250
|
+
// Amplify serves build and deploy logs the same way: `amplify:GetJob`
|
|
251
|
+
// returns a URL presigned by Amplify into an AWS-owned bucket, so the
|
|
252
|
+
// same-account rule refused it with 403. Olivia then could not read why
|
|
253
|
+
// GrowLab's frontend build failed and asked for the error 19 times while
|
|
254
|
+
// a Node-version break kept every release out of production
|
|
255
|
+
// (2026-10-26). Read-only, and only that bucket's objects.
|
|
256
|
+
s3Endpoint.addToPolicy(
|
|
257
|
+
new iam.PolicyStatement({
|
|
258
|
+
sid: "AmplifyArtifactsBucket",
|
|
259
|
+
effect: iam.Effect.ALLOW,
|
|
260
|
+
principals: [new iam.AnyPrincipal()],
|
|
261
|
+
actions: ["s3:GetObject"],
|
|
262
|
+
resources: [`arn:${this.partition}:s3:::aws-amplify-prod-${this.region}-artifacts/*`],
|
|
263
|
+
}),
|
|
264
|
+
);
|
|
250
265
|
}
|
|
251
266
|
if (!skipEndpoints.has("dynamodb")) {
|
|
252
267
|
const dynamoEndpoint = this.vpc.addGatewayEndpoint("DynamoDbEndpoint", {
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads that AWS's `ReadOnlyAccess` grants whose RESPONSES carry secrets in
|
|
3
|
+
* plaintext: environment variables, user data, connection properties, request
|
|
4
|
+
* headers, issued tokens. Both shell-reachable agent roles deny every one of
|
|
5
|
+
* them (`FoundationAgent.secretBearingReadsPolicy`), so "read-only" never means
|
|
6
|
+
* "can read the credentials that write".
|
|
7
|
+
*
|
|
8
|
+
* Found 2026-10-26 in a company account: three Lambdas held a read-write
|
|
9
|
+
* MongoDB connection string in their environment, and sampled WAF requests
|
|
10
|
+
* held the web app's admin bearer tokens. Either one turned the read-only role
|
|
11
|
+
* into a write path.
|
|
12
|
+
*
|
|
13
|
+
* Judged against `ReadOnlyAccess` v190 (2026-09-24). The rule for adding an
|
|
14
|
+
* action: its response can contain a value someone typed in as configuration
|
|
15
|
+
* (env, user data, connection or job arguments, hosted config), a credential
|
|
16
|
+
* AWS mints for the caller, or another caller's request. Data-plane reads
|
|
17
|
+
* (S3 objects, DynamoDB items, queue messages, log events) are a different
|
|
18
|
+
* question, decided separately: see docs/capabilities/aws-readonly.md.
|
|
19
|
+
*
|
|
20
|
+
* What stays readable for diagnosis: CloudWatch Logs (Insights included),
|
|
21
|
+
* metrics, alarms, X-Ray, CloudTrail, every `Describe*`/`List*` not named here,
|
|
22
|
+
* stack events and outputs, SSM parameter NAMES, and Lambda's event source
|
|
23
|
+
* mappings, aliases, concurrency and policies.
|
|
24
|
+
*
|
|
25
|
+
* Every entry is an exact action name, never a wildcard, so a reviewer can
|
|
26
|
+
* check each one against the service authorization reference.
|
|
27
|
+
*/
|
|
28
|
+
export const SECRET_BEARING_READ_ACTIONS: readonly string[] = [
|
|
29
|
+
// WAF sampled requests return full request headers, Authorization included.
|
|
30
|
+
"wafv2:GetSampledRequests",
|
|
31
|
+
"waf:GetSampledRequests",
|
|
32
|
+
"waf-regional:GetSampledRequests",
|
|
33
|
+
|
|
34
|
+
// Lambda: function configuration includes `Environment.Variables`, and
|
|
35
|
+
// GetFunction / GetLayerVersion add a presigned code download URL. List
|
|
36
|
+
// calls return the same configuration for every function or version.
|
|
37
|
+
"lambda:GetFunction",
|
|
38
|
+
"lambda:GetFunctionConfiguration",
|
|
39
|
+
"lambda:ListFunctions",
|
|
40
|
+
"lambda:ListVersionsByFunction",
|
|
41
|
+
"lambda:GetLayerVersion",
|
|
42
|
+
"lambda:GetLayerVersionByArn",
|
|
43
|
+
|
|
44
|
+
// Container and build configuration: plaintext environment variables.
|
|
45
|
+
"ecs:DescribeTaskDefinition",
|
|
46
|
+
"apprunner:DescribeService",
|
|
47
|
+
"batch:DescribeJobDefinitions",
|
|
48
|
+
"batch:DescribeJobs",
|
|
49
|
+
"codebuild:BatchGetProjects",
|
|
50
|
+
"codebuild:BatchGetBuilds",
|
|
51
|
+
"codebuild:BatchGetBuildBatches",
|
|
52
|
+
|
|
53
|
+
// Amplify apps and branches carry environment variables and basic-auth
|
|
54
|
+
// credentials; backend environments carry deployment settings.
|
|
55
|
+
"amplify:GetApp",
|
|
56
|
+
"amplify:ListApps",
|
|
57
|
+
"amplify:GetBranch",
|
|
58
|
+
"amplify:ListBranches",
|
|
59
|
+
"amplify:GetBackendEnvironment",
|
|
60
|
+
|
|
61
|
+
// Elastic Beanstalk environment properties are its environment variables.
|
|
62
|
+
"elasticbeanstalk:DescribeConfigurationSettings",
|
|
63
|
+
|
|
64
|
+
// EC2 user data (instance attribute and launch template versions).
|
|
65
|
+
"ec2:DescribeLaunchTemplateVersions",
|
|
66
|
+
"ec2:DescribeInstanceAttribute",
|
|
67
|
+
|
|
68
|
+
// SSM parameter VALUES. Denied for plain String parameters too: teams
|
|
69
|
+
// store secrets as String by mistake, and with the account's default
|
|
70
|
+
// `aws/ssm` key a SecureString can decrypt through the same call.
|
|
71
|
+
// DescribeParameters (names, types, versions) stays readable.
|
|
72
|
+
"ssm:GetParameter",
|
|
73
|
+
"ssm:GetParameters",
|
|
74
|
+
"ssm:GetParametersByPath",
|
|
75
|
+
"ssm:GetParameterHistory",
|
|
76
|
+
|
|
77
|
+
// CloudFormation template bodies can hold literal secrets and parameter
|
|
78
|
+
// defaults. Denied: DescribeStacks, DescribeStackEvents and
|
|
79
|
+
// DescribeStackResources answer the diagnosis questions without them.
|
|
80
|
+
"cloudformation:GetTemplate",
|
|
81
|
+
|
|
82
|
+
// AppConfig hosted configuration content is application configuration.
|
|
83
|
+
"appconfig:GetConfiguration",
|
|
84
|
+
"appconfig:GetHostedConfigurationVersion",
|
|
85
|
+
|
|
86
|
+
// Glue job default arguments and connection properties (JDBC user and
|
|
87
|
+
// password when not in Secrets Manager); dev endpoints carry job arguments.
|
|
88
|
+
// v190 does not grant GetConnection(s); they are named anyway because AWS
|
|
89
|
+
// revises the policy without notice and these return the password.
|
|
90
|
+
"glue:GetJob",
|
|
91
|
+
"glue:GetJobs",
|
|
92
|
+
"glue:BatchGetJobs",
|
|
93
|
+
"glue:GetConnection",
|
|
94
|
+
"glue:GetConnections",
|
|
95
|
+
"glue:GetDevEndpoint",
|
|
96
|
+
"glue:GetDevEndpoints",
|
|
97
|
+
"glue:BatchGetDevEndpoints",
|
|
98
|
+
|
|
99
|
+
// SageMaker container `Environment` maps on models and jobs.
|
|
100
|
+
"sagemaker:DescribeModel",
|
|
101
|
+
"sagemaker:DescribeTrainingJob",
|
|
102
|
+
"sagemaker:DescribeProcessingJob",
|
|
103
|
+
"sagemaker:DescribeTransformJob",
|
|
104
|
+
|
|
105
|
+
// Data Pipeline definitions hold activity fields, database passwords included.
|
|
106
|
+
"datapipeline:GetPipelineDefinition",
|
|
107
|
+
|
|
108
|
+
// AppSync: API key values, and HTTP data source authorization config.
|
|
109
|
+
"appsync:ListApiKeys",
|
|
110
|
+
"appsync:GetDataSource",
|
|
111
|
+
"appsync:ListDataSources",
|
|
112
|
+
|
|
113
|
+
// Kinesis Data Analytics / Managed Flink environment properties.
|
|
114
|
+
"kinesisanalytics:DescribeApplication",
|
|
115
|
+
|
|
116
|
+
// Lightsail bucket access keys and container service environment.
|
|
117
|
+
"lightsail:GetBucketAccessKeys",
|
|
118
|
+
"lightsail:GetContainerServices",
|
|
119
|
+
"lightsail:GetContainerServiceDeployments",
|
|
120
|
+
|
|
121
|
+
// OpsWorks app environment variables and stack custom JSON.
|
|
122
|
+
"opsworks:DescribeApps",
|
|
123
|
+
"opsworks:DescribeStacks",
|
|
124
|
+
|
|
125
|
+
// Calls that MINT a credential rather than describe one.
|
|
126
|
+
"cognito-identity:GetCredentialsForIdentity",
|
|
127
|
+
"cognito-identity:GetOpenIdToken",
|
|
128
|
+
"cognito-identity:GetOpenIdTokenForDeveloperIdentity",
|
|
129
|
+
"connect:GetFederationToken",
|
|
130
|
+
"codeartifact:GetAuthorizationToken",
|
|
131
|
+
];
|