@fjall/components-infrastructure 33.0.0 → 34.0.0
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/README.md +15 -76
- package/dist/lib/app.d.ts +1 -1
- package/dist/lib/config/aws/identityCentreConfig.d.ts +1 -1
- package/dist/lib/config/aws/identityCentreConfig.js +1 -1
- package/dist/lib/config/aws/scpPreset.js +12 -3
- package/dist/lib/lambda-assets/cert-generator/asset/index.js +126 -0
- package/dist/lib/patterns/aws/bastionFactory.js +2 -1
- package/dist/lib/patterns/aws/buildkite/buildkite.d.ts +1 -1
- package/dist/lib/patterns/aws/buildkite/buildkite.js +1 -1
- package/dist/lib/patterns/aws/buildkite/factory.d.ts +2 -2
- package/dist/lib/patterns/aws/buildkite/pins.d.ts +2 -2
- package/dist/lib/patterns/aws/buildkite/pins.js +2 -2
- package/dist/lib/patterns/aws/buildkite/schema.d.ts +1 -1
- package/dist/lib/patterns/aws/buildkite/schema.js +1 -1
- package/dist/lib/patterns/aws/buildkite/userData.d.ts +1 -1
- package/dist/lib/patterns/aws/buildkite/userData.js +1 -1
- package/dist/lib/patterns/aws/buildkite.d.ts +1 -1
- package/dist/lib/patterns/aws/buildkite.js +1 -1
- package/dist/lib/patterns/aws/cdn.d.ts +4 -1
- package/dist/lib/patterns/aws/clickhouseDatabase.js +6 -5
- package/dist/lib/patterns/aws/clickhouseTls/types.d.ts +1 -1
- package/dist/lib/patterns/aws/compute.d.ts +2 -2
- package/dist/lib/patterns/aws/computeEc2.d.ts +5 -18
- package/dist/lib/patterns/aws/computeEc2.js +9 -3
- package/dist/lib/patterns/aws/computeEcs.d.ts +4 -4
- package/dist/lib/patterns/aws/computeEcs.js +5 -9
- package/dist/lib/patterns/aws/computeEcsTypes.d.ts +5 -5
- package/dist/lib/patterns/aws/computePropApplicability.js +1 -1
- package/dist/lib/patterns/aws/fivetranProxy.d.ts +8 -3
- package/dist/lib/patterns/aws/fivetranProxy.js +9 -6
- package/dist/lib/patterns/aws/interfaces/compute.d.ts +8 -0
- package/dist/lib/patterns/aws/payload.js +6 -2
- package/dist/lib/patterns/aws/storage.d.ts +8 -0
- package/dist/lib/patterns/aws/storage.js +1 -0
- package/dist/lib/resources/aws/cdn/cloudFront.d.ts +16 -0
- package/dist/lib/resources/aws/cdn/cloudFront.js +52 -2
- package/dist/lib/resources/aws/cdn/responseHeadersPolicy.d.ts +1 -1
- package/dist/lib/resources/aws/cdn/responseHeadersPolicy.js +1 -1
- package/dist/lib/resources/aws/compute/ec2.d.ts +35 -8
- package/dist/lib/resources/aws/compute/ec2.js +64 -15
- package/dist/lib/resources/aws/compute/ec2SshAccess.d.ts +80 -0
- package/dist/lib/resources/aws/compute/ec2SshAccess.js +166 -0
- package/dist/lib/resources/aws/compute/ecs.d.ts +11 -2
- package/dist/lib/resources/aws/compute/ecs.js +29 -6
- package/dist/lib/resources/aws/compute/lambda.js +1 -3
- package/dist/lib/resources/aws/database/clickhouseConstants.d.ts +5 -5
- package/dist/lib/resources/aws/database/clickhouseConstants.js +5 -5
- package/dist/lib/resources/aws/database/dynamodb.js +5 -6
- package/dist/lib/resources/aws/iam/identityCenter/user.d.ts +1 -1
- package/dist/lib/resources/aws/iam/identityCenter/user.js +1 -1
- package/dist/lib/resources/aws/logging/accessLogBucket.d.ts +5 -3
- package/dist/lib/resources/aws/logging/accessLogBucket.js +6 -3
- package/dist/lib/resources/aws/messaging/sns.d.ts +17 -7
- package/dist/lib/resources/aws/messaging/sns.js +4 -4
- package/dist/lib/resources/aws/secrets/stackEncryptionKey.d.ts +17 -4
- package/dist/lib/resources/aws/secrets/stackEncryptionKey.js +20 -4
- package/dist/lib/resources/aws/storage/bucketDeployment.d.ts +2 -3
- package/dist/lib/resources/aws/storage/bucketDeployment.js +2 -3
- package/dist/lib/resources/aws/storage/s3.d.ts +14 -1
- package/dist/lib/resources/aws/storage/s3.js +6 -3
- package/package.json +6 -4
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { Annotations, Token } from "aws-cdk-lib";
|
|
2
|
+
import { SubnetType } from "aws-cdk-lib/aws-ec2";
|
|
3
|
+
import { CIDR_REGEX, WORLD_OPEN_IPV4_CIDR } from "@fjall/generator";
|
|
4
|
+
import { mintedEc2KeyPairName } from "@fjall/util";
|
|
5
|
+
/**
|
|
6
|
+
* The construct prop and the generator's `Ec2SshAccessSchema` are one contract
|
|
7
|
+
* spelled twice: the plan's `sshAccess` projects literally into this prop.
|
|
8
|
+
* This map fails typecheck until both sides agree: a key missing from or
|
|
9
|
+
* added to either side, or a field whose type drifts between them (`true` is
|
|
10
|
+
* not assignable to the `never` a drifted field resolves to). Its keys are
|
|
11
|
+
* also the runtime accept-set for a caller-supplied object.
|
|
12
|
+
*/
|
|
13
|
+
const SSH_ACCESS_FIELD_PARITY = {
|
|
14
|
+
allowedIpCidrs: true,
|
|
15
|
+
keyName: true,
|
|
16
|
+
public: true
|
|
17
|
+
};
|
|
18
|
+
const SSH_ACCESS_KEYS = Object.keys(SSH_ACCESS_FIELD_PARITY);
|
|
19
|
+
export const EC2_WORLD_OPEN_SSH_WARNING_ID = "@fjall/components-infrastructure:ec2WorldOpenSsh";
|
|
20
|
+
export const EC2_RETIRED_SSH_PROP_WARNING_ID = "@fjall/components-infrastructure:ec2RetiredSshProp";
|
|
21
|
+
/**
|
|
22
|
+
* One retired spelling per owner, read the way its pre-34 owner read it:
|
|
23
|
+
* `Ec2Instance` tested `enableSSH` for truthiness; `Ec2Compute` treated any
|
|
24
|
+
* `ssh` other than `undefined` or `false` as SSH on, and forwarded neither
|
|
25
|
+
* its `keyName` nor its `allowedCidrs`.
|
|
26
|
+
*/
|
|
27
|
+
const RETIRED_SSH_PROPS = {
|
|
28
|
+
Ec2Instance: {
|
|
29
|
+
prop: "enableSSH",
|
|
30
|
+
askedForDefault: (value) => !value,
|
|
31
|
+
translation: `enableSSH: true meant sshAccess: { allowedIpCidrs: ["${WORLD_OPEN_IPV4_CIDR}"], public: true }.`
|
|
32
|
+
},
|
|
33
|
+
Ec2Compute: {
|
|
34
|
+
prop: "ssh",
|
|
35
|
+
askedForDefault: (value) => value === undefined || value === false,
|
|
36
|
+
translation: `ssh: {} meant sshAccess: { allowedIpCidrs: ["${WORLD_OPEN_IPV4_CIDR}"], public: true }; its keyName and allowedCidrs were never forwarded. ` +
|
|
37
|
+
"Carry them into sshAccess only to have them honoured now: allowedIpCidrs narrows the rule, and naming a key pair switches the group " +
|
|
38
|
+
"from the key pair this construct mints to the named one, so CloudFormation deletes the minted key pair and its stored private key."
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* The retired spelling is read at synth, not only at typecheck: a JavaScript
|
|
43
|
+
* caller, or a spread of an older props object, would otherwise deploy with
|
|
44
|
+
* no SSH and no signal. A value the pre-34 owner read as SSH on asked for an
|
|
45
|
+
* exposure the new shape must spell out, so it throws with the translation.
|
|
46
|
+
* A value it read as off asked for the default and gets it, with a warning
|
|
47
|
+
* to delete the prop: refusing a no-op would strand every file the pre-34
|
|
48
|
+
* wizard wrote.
|
|
49
|
+
*/
|
|
50
|
+
export function checkRetiredSshProp(scope, owner, props) {
|
|
51
|
+
const retired = RETIRED_SSH_PROPS[owner];
|
|
52
|
+
if (!(retired.prop in props))
|
|
53
|
+
return;
|
|
54
|
+
const value = props[retired.prop];
|
|
55
|
+
if (retired.askedForDefault(value)) {
|
|
56
|
+
Annotations.of(scope).addWarningV2(EC2_RETIRED_SSH_PROP_WARNING_ID, `${owner}.${retired.prop} was removed in @fjall/components-infrastructure 34; ${retired.prop}: ${String(value)} asked for the default (no SSH), which still applies. Delete the prop.`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`${owner}.${retired.prop} was removed in @fjall/components-infrastructure 34. ` +
|
|
60
|
+
"Declare sshAccess: { allowedIpCidrs: [...], keyName?, public? }, or omit it for Session Manager-only access. " +
|
|
61
|
+
retired.translation);
|
|
62
|
+
}
|
|
63
|
+
function listTokenError(where) {
|
|
64
|
+
return new Error(`Ec2Instance.sshAccess.${where} cannot be a list token (CfnParameter.valueAsList, Fn.split over a parameter): ` +
|
|
65
|
+
"each range becomes one ingress rule, so the list itself must be literal at synth. " +
|
|
66
|
+
"A token per element, such as vpc.vpcCidrBlock, is fine.");
|
|
67
|
+
}
|
|
68
|
+
/** Synth-time contract for `sshAccess`; every refusal names the cure. */
|
|
69
|
+
export function validateSshAccess(props) {
|
|
70
|
+
const access = props.sshAccess;
|
|
71
|
+
if (access === undefined)
|
|
72
|
+
return;
|
|
73
|
+
const raw = access;
|
|
74
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
75
|
+
const got = raw === null
|
|
76
|
+
? "null"
|
|
77
|
+
: Array.isArray(raw)
|
|
78
|
+
? "an array"
|
|
79
|
+
: `a ${typeof raw}`;
|
|
80
|
+
throw new Error(`Ec2Instance.sshAccess must be an object { allowedIpCidrs, keyName?, public? }; got ${got}. Omit it for Session Manager-only access.`);
|
|
81
|
+
}
|
|
82
|
+
if (props.securityGroup !== undefined) {
|
|
83
|
+
throw new Error("Ec2Instance.sshAccess cannot be combined with a caller-supplied securityGroup. " +
|
|
84
|
+
"sshAccess adds port-22 ingress rules to the ASG security group; routing them onto an " +
|
|
85
|
+
"externally-owned security group couples this construct's permissions onto a SG whose lifecycle it " +
|
|
86
|
+
"does not own. Either omit securityGroup (a Fjall-owned SG will be created) or omit sshAccess and " +
|
|
87
|
+
"add the port-22 rule to your own security group.");
|
|
88
|
+
}
|
|
89
|
+
const unknownKey = Object.keys(access).find((key) => !SSH_ACCESS_KEYS.includes(key));
|
|
90
|
+
if (unknownKey !== undefined) {
|
|
91
|
+
throw new Error(`Ec2Instance.sshAccess has no field '${unknownKey}'; its fields are ${SSH_ACCESS_KEYS.join(", ")}. ` +
|
|
92
|
+
"The pre-34 ssh prop's allowedCidrs is allowedIpCidrs here.");
|
|
93
|
+
}
|
|
94
|
+
if (Token.isUnresolved(access.allowedIpCidrs)) {
|
|
95
|
+
throw listTokenError("allowedIpCidrs");
|
|
96
|
+
}
|
|
97
|
+
if (!Array.isArray(access.allowedIpCidrs) ||
|
|
98
|
+
access.allowedIpCidrs.length === 0) {
|
|
99
|
+
throw new Error("Ec2Instance.sshAccess.allowedIpCidrs must list at least one IPv4 CIDR. " +
|
|
100
|
+
"SSH that no address can reach is not a configuration — omit sshAccess for no SSH.");
|
|
101
|
+
}
|
|
102
|
+
const seen = new Map();
|
|
103
|
+
access.allowedIpCidrs.forEach((cidr, index) => {
|
|
104
|
+
if (typeof cidr !== "string" || cidr === "") {
|
|
105
|
+
throw new Error(`Ec2Instance.sshAccess.allowedIpCidrs[${index}] must be a non-empty IPv4 CIDR string`);
|
|
106
|
+
}
|
|
107
|
+
// A list token spread into a literal list ([...ranges.valueAsList, "10.0.0.0/8"])
|
|
108
|
+
// carries the list marker in one element; the whole-list check above cannot see it.
|
|
109
|
+
if (Token.isUnresolved([cidr])) {
|
|
110
|
+
throw listTokenError(`allowedIpCidrs[${index}]`);
|
|
111
|
+
}
|
|
112
|
+
if (!Token.isUnresolved(cidr) && !CIDR_REGEX.test(cidr)) {
|
|
113
|
+
throw new Error(`Ec2Instance.sshAccess.allowedIpCidrs[${index}] '${cidr}' is not an IPv4 CIDR (dotted-quad/prefix-length, e.g. 203.0.113.4/32)`);
|
|
114
|
+
}
|
|
115
|
+
const first = seen.get(cidr);
|
|
116
|
+
if (first !== undefined) {
|
|
117
|
+
throw new Error(`Ec2Instance.sshAccess.allowedIpCidrs[${index}] duplicates allowedIpCidrs[${first}] ('${cidr}'); each range is one ingress rule and AWS rejects a duplicate rule`);
|
|
118
|
+
}
|
|
119
|
+
seen.set(cidr, index);
|
|
120
|
+
});
|
|
121
|
+
const minted = mintedEc2KeyPairName(props.serviceName);
|
|
122
|
+
if (access.keyName === "") {
|
|
123
|
+
throw new Error(`Ec2Instance.sshAccess.keyName must not be empty; omit it to mint a key pair named ${minted}`);
|
|
124
|
+
}
|
|
125
|
+
if (access.keyName === minted) {
|
|
126
|
+
throw new Error(`Ec2Instance.sshAccess.keyName '${minted}' is the name of the key pair this construct mints for serviceName '${props.serviceName}'. ` +
|
|
127
|
+
"Omit keyName to keep the managed key pair: naming it switches the construct to an imported reference, and CloudFormation then deletes the managed key pair and its stored private key.");
|
|
128
|
+
}
|
|
129
|
+
if (access.public === true) {
|
|
130
|
+
if (props.associatePublicIpAddress === false) {
|
|
131
|
+
throw new Error("Ec2Instance.sshAccess.public: true assigns a public IP, but associatePublicIpAddress is false. Drop one of them.");
|
|
132
|
+
}
|
|
133
|
+
const selection = props.vpcSubnets;
|
|
134
|
+
if (selection !== undefined) {
|
|
135
|
+
if (selection.subnetType !== undefined &&
|
|
136
|
+
selection.subnetType !== SubnetType.PUBLIC) {
|
|
137
|
+
throw new Error(`Ec2Instance.sshAccess.public: true places the instances in public subnets, but vpcSubnets.subnetType is ${selection.subnetType}. Drop one of them.`);
|
|
138
|
+
}
|
|
139
|
+
if (selection.subnetGroupName !== undefined ||
|
|
140
|
+
selection.subnets !== undefined) {
|
|
141
|
+
throw new Error("Ec2Instance.sshAccess.public: true places the instances in the VPC's public subnets; vpcSubnets may pin availabilityZones but cannot select subnets by group name or instance. Set public: false and own the placement, or drop the selector.");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Whether port 22 faces the internet. An instance is reachable from outside
|
|
148
|
+
* only with a public IP AND a subnet routed to an internet gateway, so both
|
|
149
|
+
* halves of the resolved placement must say public. `public: true` sets
|
|
150
|
+
* both; a caller who owns the placement through `vpcSubnets` and
|
|
151
|
+
* `associatePublicIpAddress` reaches the same exposure without the flag,
|
|
152
|
+
* which is why the warning reads the placement rather than `sshAccess.public`.
|
|
153
|
+
*/
|
|
154
|
+
export function sshFacesInternet(placement) {
|
|
155
|
+
return placement.associatePublicIpAddress && placement.hasPublicSubnets;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The world-open warning: a `0.0.0.0/0` range still lands (the caller asked
|
|
159
|
+
* for it) but synthesises with an acknowledgeable warning, as
|
|
160
|
+
* `rdsWorldOpenIngress` does for a database.
|
|
161
|
+
*/
|
|
162
|
+
export function worldOpenSshWarning(serviceName, facesInternet) {
|
|
163
|
+
return facesInternet
|
|
164
|
+
? `Ec2Instance '${serviceName}': sshAccess.allowedIpCidrs contains '${WORLD_OPEN_IPV4_CIDR}' and the instances are publicly reachable, so port 22 is open to the entire internet. Scope allowedIpCidrs to the addresses that need access (e.g. "203.0.113.4/32").`
|
|
165
|
+
: `Ec2Instance '${serviceName}': sshAccess.allowedIpCidrs contains '${WORLD_OPEN_IPV4_CIDR}'. The instances are not reachable from the internet as placed, so the rule admits every address that can route to them (the VPC, peered networks, VPNs); scope allowedIpCidrs to the ranges that need access (e.g. "10.0.0.0/8").`;
|
|
166
|
+
}
|
|
@@ -2,6 +2,7 @@ import { Cluster as CdkCluster, type FargateService, type Ec2Service, type TaskD
|
|
|
2
2
|
import { Connections, type IConnectable } from "aws-cdk-lib/aws-ec2";
|
|
3
3
|
import { Construct } from "constructs";
|
|
4
4
|
import type { StackBuilder } from "../base/awsStack.js";
|
|
5
|
+
import { Grant, type IGrantable } from "aws-cdk-lib/aws-iam";
|
|
5
6
|
import type { ApplicationListener, ApplicationLoadBalancer } from "aws-cdk-lib/aws-elasticloadbalancingv2";
|
|
6
7
|
import { StackEncryptionKey } from "../secrets/stackEncryptionKey.js";
|
|
7
8
|
import { type EcsClusterProps } from "./ecsTypes.js";
|
|
@@ -70,8 +71,8 @@ export default class EcsCluster extends Construct implements IConnectable {
|
|
|
70
71
|
/**
|
|
71
72
|
* The stack's shared key when the app backup tier encrypts (resolved once,
|
|
72
73
|
* here); undefined otherwise. Read by the patterns layer for the log
|
|
73
|
-
* groups it creates beside the cluster's own
|
|
74
|
-
* one place.
|
|
74
|
+
* groups it creates beside the cluster's own and by `grantExecuteCommand`,
|
|
75
|
+
* so the tier is consulted in one place.
|
|
75
76
|
*/
|
|
76
77
|
readonly encryptionKey: StackEncryptionKey | undefined;
|
|
77
78
|
private readonly loadBalancerDeletionProtection;
|
|
@@ -111,6 +112,14 @@ export default class EcsCluster extends Construct implements IConnectable {
|
|
|
111
112
|
registerScheduledTaskDefinition(name: string, taskDefinition: TaskDefinition): void;
|
|
112
113
|
/** Get the ECS cluster construct. */
|
|
113
114
|
getCluster(): CdkCluster;
|
|
115
|
+
/**
|
|
116
|
+
* Grant what an ECS Exec caller needs on this cluster: `ecs:ExecuteCommand`
|
|
117
|
+
* on the cluster and its tasks and, when the tier encrypts the session,
|
|
118
|
+
* `kms:GenerateDataKey` on the stack key — the caller-side half of ECS Exec
|
|
119
|
+
* encryption (the task role's Decrypt rides CDK's BaseService wiring).
|
|
120
|
+
* https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-exec.html
|
|
121
|
+
*/
|
|
122
|
+
grantExecuteCommand(grantee: IGrantable): Grant;
|
|
114
123
|
/**
|
|
115
124
|
* Connectable scoped to the cluster's EC2 capacity security groups alone.
|
|
116
125
|
* Scheduled tasks run on the cluster's instances (bridge/host networking),
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Cluster as CdkCluster, ContainerInsights, ExecuteCommandLogging } from "aws-cdk-lib/aws-ecs";
|
|
2
2
|
import { Connections, Port } from "aws-cdk-lib/aws-ec2";
|
|
3
3
|
import { Construct } from "constructs";
|
|
4
|
-
import { CfnOutput, Aspects } from "aws-cdk-lib";
|
|
4
|
+
import { CfnOutput, Aspects, Stack } from "aws-cdk-lib";
|
|
5
|
+
import { Grant } from "aws-cdk-lib/aws-iam";
|
|
5
6
|
import { processConnections } from "../../../utils/connections.js";
|
|
6
7
|
import { tierDefault } from "../../../utils/backupTierMapping.js";
|
|
7
8
|
import { envAwareRemovalPolicyDefault } from "../../../utils/removalPolicy.js";
|
|
@@ -91,8 +92,8 @@ export default class EcsCluster extends Construct {
|
|
|
91
92
|
/**
|
|
92
93
|
* The stack's shared key when the app backup tier encrypts (resolved once,
|
|
93
94
|
* here); undefined otherwise. Read by the patterns layer for the log
|
|
94
|
-
* groups it creates beside the cluster's own
|
|
95
|
-
* one place.
|
|
95
|
+
* groups it creates beside the cluster's own and by `grantExecuteCommand`,
|
|
96
|
+
* so the tier is consulted in one place.
|
|
96
97
|
*/
|
|
97
98
|
encryptionKey;
|
|
98
99
|
loadBalancerDeletionProtection;
|
|
@@ -116,9 +117,7 @@ export default class EcsCluster extends Construct {
|
|
|
116
117
|
const tier = props.appBackupTier;
|
|
117
118
|
this.containerInsightsEnabled =
|
|
118
119
|
props.containerInsights ?? tierDefault(tier, "containerInsights");
|
|
119
|
-
this.encryptionKey =
|
|
120
|
-
? StackEncryptionKey.of(this)
|
|
121
|
-
: undefined;
|
|
120
|
+
this.encryptionKey = StackEncryptionKey.forTier(this, tier);
|
|
122
121
|
this.loadBalancerDeletionProtection =
|
|
123
122
|
props.cluster?.loadBalancerDeletionProtection ??
|
|
124
123
|
(tierDefault(tier, "deletionProtection") &&
|
|
@@ -254,6 +253,30 @@ export default class EcsCluster extends Construct {
|
|
|
254
253
|
getCluster() {
|
|
255
254
|
return this.cluster;
|
|
256
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Grant what an ECS Exec caller needs on this cluster: `ecs:ExecuteCommand`
|
|
258
|
+
* on the cluster and its tasks and, when the tier encrypts the session,
|
|
259
|
+
* `kms:GenerateDataKey` on the stack key — the caller-side half of ECS Exec
|
|
260
|
+
* encryption (the task role's Decrypt rides CDK's BaseService wiring).
|
|
261
|
+
* https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-exec.html
|
|
262
|
+
*/
|
|
263
|
+
grantExecuteCommand(grantee) {
|
|
264
|
+
const exec = Grant.addToPrincipal({
|
|
265
|
+
grantee,
|
|
266
|
+
actions: ["ecs:ExecuteCommand"],
|
|
267
|
+
resourceArns: [
|
|
268
|
+
this.cluster.clusterArn,
|
|
269
|
+
Stack.of(this).formatArn({
|
|
270
|
+
service: "ecs",
|
|
271
|
+
resource: "task",
|
|
272
|
+
resourceName: `${this.props.clusterName}/*`
|
|
273
|
+
})
|
|
274
|
+
]
|
|
275
|
+
});
|
|
276
|
+
return this.encryptionKey === undefined
|
|
277
|
+
? exec
|
|
278
|
+
: exec.combine(this.encryptionKey.key.grant(grantee, "kms:GenerateDataKey"));
|
|
279
|
+
}
|
|
257
280
|
/**
|
|
258
281
|
* Connectable scoped to the cluster's EC2 capacity security groups alone.
|
|
259
282
|
* Scheduled tasks run on the cluster's instances (bridge/host networking),
|
|
@@ -178,9 +178,7 @@ export class LambdaFunction extends Function {
|
|
|
178
178
|
const tracingEnabled = tracing ?? tierDefault(appBackupTier, "lambdaTracing");
|
|
179
179
|
// Grant-based: Lambda encrypts the environment through a KMS grant the
|
|
180
180
|
// deploying role creates, so the execution role needs no key policy.
|
|
181
|
-
const encryptionKey =
|
|
182
|
-
? StackEncryptionKey.of(scope)
|
|
183
|
-
: undefined;
|
|
181
|
+
const encryptionKey = StackEncryptionKey.forTier(scope, appBackupTier);
|
|
184
182
|
super(scope, id, {
|
|
185
183
|
...cdkProps,
|
|
186
184
|
vpcSubnets,
|
|
@@ -155,7 +155,7 @@ export declare const CLICKHOUSE_NATIVE_PORT = 9000;
|
|
|
155
155
|
export declare const CLICKHOUSE_PROMETHEUS_PORT = 9363;
|
|
156
156
|
/** TLS-mode ClickHouse ports.
|
|
157
157
|
* HTTPS replaces 8123; secure native protocol replaces 9000.
|
|
158
|
-
* See
|
|
158
|
+
* See the ClickHouse TLS pattern, § "The contract". */
|
|
159
159
|
export declare const CLICKHOUSE_HTTPS_PORT = 8443;
|
|
160
160
|
export declare const CLICKHOUSE_TCP_SECURE_PORT = 9440;
|
|
161
161
|
/** Mount path inside the ClickHouse container for the materialised TLS cert
|
|
@@ -188,8 +188,8 @@ export declare function clickHouseUserSecretName(userName: string): string;
|
|
|
188
188
|
* Canonical source is `@fjall/util/migration § userPasswordEnvName` — re-exported
|
|
189
189
|
* here so the construct-side import chain stays unchanged while the runner
|
|
190
190
|
* (`webapp/scripts/migration-runner.mjs`) and any future consumer share a single
|
|
191
|
-
* declaration. Coupled values
|
|
192
|
-
*
|
|
191
|
+
* declaration. Coupled values (the shared-source-at-two-occurrences
|
|
192
|
+
* rule). */
|
|
193
193
|
export { userPasswordEnvName } from "@fjall/util/migration";
|
|
194
194
|
/** Env var name carrying the SHA-256 of a user's plaintext password.
|
|
195
195
|
* The container entrypoint wrapper derives this name from the
|
|
@@ -305,8 +305,8 @@ export declare const BACKUP_RETENTION_DAYS = 14;
|
|
|
305
305
|
* wrote can be read back. Dropped on the way in and on the way out; never
|
|
306
306
|
* queried by the application. Canonical source is `@fjall/util/migration`
|
|
307
307
|
* because `@fjall/clickhouse` grants `fjall_maintenance` DROP on exactly
|
|
308
|
-
* this database — coupled values
|
|
309
|
-
*
|
|
308
|
+
* this database — coupled values (the shared-source-at-two-occurrences
|
|
309
|
+
* rule). */
|
|
310
310
|
export { CLICKHOUSE_BACKUP_SCRATCH_DATABASE } from "@fjall/util/migration";
|
|
311
311
|
/** The two scheduled maintenance jobs that authenticate as
|
|
312
312
|
* `fjall_maintenance`. Drives the per-job log-group → metric-filter wiring
|
|
@@ -175,7 +175,7 @@ export const CLICKHOUSE_NATIVE_PORT = 9000;
|
|
|
175
175
|
export const CLICKHOUSE_PROMETHEUS_PORT = 9363;
|
|
176
176
|
/** TLS-mode ClickHouse ports.
|
|
177
177
|
* HTTPS replaces 8123; secure native protocol replaces 9000.
|
|
178
|
-
* See
|
|
178
|
+
* See the ClickHouse TLS pattern, § "The contract". */
|
|
179
179
|
export const CLICKHOUSE_HTTPS_PORT = 8443;
|
|
180
180
|
export const CLICKHOUSE_TCP_SECURE_PORT = 9440;
|
|
181
181
|
/** Mount path inside the ClickHouse container for the materialised TLS cert
|
|
@@ -210,8 +210,8 @@ export function clickHouseUserSecretName(userName) {
|
|
|
210
210
|
* Canonical source is `@fjall/util/migration § userPasswordEnvName` — re-exported
|
|
211
211
|
* here so the construct-side import chain stays unchanged while the runner
|
|
212
212
|
* (`webapp/scripts/migration-runner.mjs`) and any future consumer share a single
|
|
213
|
-
* declaration. Coupled values
|
|
214
|
-
*
|
|
213
|
+
* declaration. Coupled values (the shared-source-at-two-occurrences
|
|
214
|
+
* rule). */
|
|
215
215
|
export { userPasswordEnvName } from "@fjall/util/migration";
|
|
216
216
|
/** Env var name carrying the SHA-256 of a user's plaintext password.
|
|
217
217
|
* The container entrypoint wrapper derives this name from the
|
|
@@ -347,8 +347,8 @@ export const BACKUP_RETENTION_DAYS = 14;
|
|
|
347
347
|
* wrote can be read back. Dropped on the way in and on the way out; never
|
|
348
348
|
* queried by the application. Canonical source is `@fjall/util/migration`
|
|
349
349
|
* because `@fjall/clickhouse` grants `fjall_maintenance` DROP on exactly
|
|
350
|
-
* this database — coupled values
|
|
351
|
-
*
|
|
350
|
+
* this database — coupled values (the shared-source-at-two-occurrences
|
|
351
|
+
* rule). */
|
|
352
352
|
export { CLICKHOUSE_BACKUP_SCRATCH_DATABASE } from "@fjall/util/migration";
|
|
353
353
|
/** The two scheduled maintenance jobs that authenticate as
|
|
354
354
|
* `fjall_maintenance`. Drives the per-job log-group → metric-filter wiring
|
|
@@ -66,10 +66,11 @@ export class DynamoDBTable extends Construct {
|
|
|
66
66
|
constructor(scope, id, props) {
|
|
67
67
|
super(scope, id);
|
|
68
68
|
this.id = id;
|
|
69
|
+
const stackKey = StackEncryptionKey.forTier(this, props.appBackupTier, props.encryption === undefined
|
|
70
|
+
? undefined
|
|
71
|
+
: props.encryption === "CUSTOMER_MANAGED");
|
|
69
72
|
const encryption = props.encryption ??
|
|
70
|
-
(
|
|
71
|
-
? "CUSTOMER_MANAGED"
|
|
72
|
-
: "AWS_MANAGED");
|
|
73
|
+
(stackKey === undefined ? "AWS_MANAGED" : "CUSTOMER_MANAGED");
|
|
73
74
|
const deletionProtection = props.deletionProtection ??
|
|
74
75
|
tierDefault(props.appBackupTier, "deletionProtection");
|
|
75
76
|
this.table = new Table(this, `${id}Table`, {
|
|
@@ -92,9 +93,7 @@ export class DynamoDBTable extends Construct {
|
|
|
92
93
|
pointInTimeRecoveryEnabled: props.pointInTimeRecovery ?? true
|
|
93
94
|
},
|
|
94
95
|
encryption: toTableEncryption(encryption),
|
|
95
|
-
...(
|
|
96
|
-
encryptionKey: StackEncryptionKey.of(this).key
|
|
97
|
-
}),
|
|
96
|
+
...(stackKey !== undefined && { encryptionKey: stackKey.key }),
|
|
98
97
|
deletionProtection,
|
|
99
98
|
removalPolicy: toRemovalPolicy(props.removalPolicy),
|
|
100
99
|
timeToLiveAttribute: props.timeToLiveAttribute
|
|
@@ -8,7 +8,7 @@ export interface IdentityStoreUserProviderProps {
|
|
|
8
8
|
* Shared Provider-framework backend for Custom::FjallIdentityStoreUser —
|
|
9
9
|
* one handler + provider pair serves every declared user in the stack. A
|
|
10
10
|
* plain AwsCustomResource cannot express the create-or-adopt branching the
|
|
11
|
-
* handler implements (
|
|
11
|
+
* handler implements (the 2026-07-04 organisation user-management design, § 2).
|
|
12
12
|
*/
|
|
13
13
|
export declare class IdentityStoreUserProvider extends Construct {
|
|
14
14
|
readonly serviceToken: string;
|
|
@@ -23,7 +23,7 @@ export function identityStoreUserArnPattern(partition) {
|
|
|
23
23
|
* Shared Provider-framework backend for Custom::FjallIdentityStoreUser —
|
|
24
24
|
* one handler + provider pair serves every declared user in the stack. A
|
|
25
25
|
* plain AwsCustomResource cannot express the create-or-adopt branching the
|
|
26
|
-
* handler implements (
|
|
26
|
+
* handler implements (the 2026-07-04 organisation user-management design, § 2).
|
|
27
27
|
*/
|
|
28
28
|
export class IdentityStoreUserProvider extends Construct {
|
|
29
29
|
serviceToken;
|
|
@@ -15,8 +15,10 @@ export declare const ACCESS_LOG_RETENTION_DAYS = 90;
|
|
|
15
15
|
* tier-aware: ELB log delivery requires SSE-S3 (it cannot write to a
|
|
16
16
|
* KMS-encrypted bucket), CloudFront standard logging requires ACLs enabled
|
|
17
17
|
* (`OBJECT_WRITER` ownership, the CDK's own logging-bucket shape), and
|
|
18
|
-
* versioning would only multiply expired log objects
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* versioning would only multiply expired log objects — the `access-logs`
|
|
19
|
+
* exemption marker tells posture so, landing `S3_VERSIONING_DISABLED`
|
|
20
|
+
* `mitigated` rather than `open`. The bucket takes the env-aware removal
|
|
21
|
+
* policy like every other bucket: production RETAINs its audit evidence,
|
|
22
|
+
* non-production DESTROYs.
|
|
21
23
|
*/
|
|
22
24
|
export declare function stackAccessLogBucket(scope: Construct): S3Bucket;
|
|
@@ -17,9 +17,11 @@ export const ACCESS_LOG_RETENTION_DAYS = 90;
|
|
|
17
17
|
* tier-aware: ELB log delivery requires SSE-S3 (it cannot write to a
|
|
18
18
|
* KMS-encrypted bucket), CloudFront standard logging requires ACLs enabled
|
|
19
19
|
* (`OBJECT_WRITER` ownership, the CDK's own logging-bucket shape), and
|
|
20
|
-
* versioning would only multiply expired log objects
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* versioning would only multiply expired log objects — the `access-logs`
|
|
21
|
+
* exemption marker tells posture so, landing `S3_VERSIONING_DISABLED`
|
|
22
|
+
* `mitigated` rather than `open`. The bucket takes the env-aware removal
|
|
23
|
+
* policy like every other bucket: production RETAINs its audit evidence,
|
|
24
|
+
* non-production DESTROYs.
|
|
23
25
|
*/
|
|
24
26
|
export function stackAccessLogBucket(scope) {
|
|
25
27
|
const stack = Stack.of(scope);
|
|
@@ -30,6 +32,7 @@ export function stackAccessLogBucket(scope) {
|
|
|
30
32
|
encryption: BucketEncryption.S3_MANAGED,
|
|
31
33
|
objectOwnership: ObjectOwnership.OBJECT_WRITER,
|
|
32
34
|
versioned: false,
|
|
35
|
+
versioningExemption: "access-logs",
|
|
33
36
|
lifecycleRules: [
|
|
34
37
|
{
|
|
35
38
|
id: "ExpireAccessLogs",
|
|
@@ -24,13 +24,23 @@ export interface SNSTopicProps {
|
|
|
24
24
|
*/
|
|
25
25
|
signatureVersion?: "1" | "2";
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* the
|
|
33
|
-
*
|
|
27
|
+
* Server-side encryption at rest. Absent ⇒ the app backup tier decides
|
|
28
|
+
* through its `customerManagedKeys` column: `"SSE_KMS"` at enterprise,
|
|
29
|
+
* `"NONE"` at every other tier. `"SSE_KMS"` encrypts with the stack's
|
|
30
|
+
* shared [[StackEncryptionKey]] (its SNS service publishers admitted)
|
|
31
|
+
* unless `masterKey` brings a key of your own; `"NONE"` at enterprise is
|
|
32
|
+
* the auditable opt-out — it sits in the declaration, so the
|
|
33
|
+
* SNS_TOPIC_NOT_ENCRYPTED finding it leaves is a choice, not an omission.
|
|
34
|
+
* Below enterprise the topic stays unencrypted by default — the accessible
|
|
35
|
+
* default (a customer-managed key costs $1–3/month per account-region once
|
|
36
|
+
* annual rotation accrues), so the finding stands as the signpost to the
|
|
37
|
+
* tier rather than being silently pre-paid.
|
|
38
|
+
*/
|
|
39
|
+
encryption?: "SSE_KMS" | "NONE";
|
|
40
|
+
/**
|
|
41
|
+
* Bring your own KMS key for encryption at rest. Beats `encryption` and
|
|
42
|
+
* the tier: the topic encrypts with this key and the stack's shared key is
|
|
43
|
+
* not minted for it.
|
|
34
44
|
*
|
|
35
45
|
* Every AWS service principal that publishes to the topic needs
|
|
36
46
|
* `kms:Decrypt` + `kms:GenerateDataKey*` on this key's RESOURCE policy or
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
2
|
import { CfnOutput } from "aws-cdk-lib";
|
|
3
3
|
import { Topic } from "aws-cdk-lib/aws-sns";
|
|
4
|
-
import { tierDefault } from "../../../utils/backupTierMapping.js";
|
|
5
4
|
import { StackEncryptionKey } from "../secrets/stackEncryptionKey.js";
|
|
6
5
|
import { toRemovalPolicy } from "./utils.js";
|
|
7
6
|
export class SNSTopic extends Construct {
|
|
@@ -11,10 +10,11 @@ export class SNSTopic extends Construct {
|
|
|
11
10
|
super(scope, id);
|
|
12
11
|
this.id = id;
|
|
13
12
|
const isFifo = props.fifo === true;
|
|
13
|
+
const encryptionOverride = props.encryption === undefined
|
|
14
|
+
? undefined
|
|
15
|
+
: props.encryption === "SSE_KMS";
|
|
14
16
|
const masterKey = props.masterKey ??
|
|
15
|
-
(
|
|
16
|
-
? StackEncryptionKey.of(this).forSnsServicePublishers()
|
|
17
|
-
: undefined);
|
|
17
|
+
StackEncryptionKey.forTier(this, props.appBackupTier, encryptionOverride)?.forSnsServicePublishers();
|
|
18
18
|
const topicName = props.topicName
|
|
19
19
|
? isFifo
|
|
20
20
|
? `${props.topicName}.fifo`
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IKey } from "aws-cdk-lib/aws-kms";
|
|
2
2
|
import type { Construct } from "constructs";
|
|
3
|
+
import { type BackupTier } from "../../../utils/backupTierMapping.js";
|
|
3
4
|
import { CustomerManagedKey } from "./kms.js";
|
|
4
5
|
/**
|
|
5
6
|
* The one customer-managed KMS key an enterprise-tier stack encrypts its
|
|
@@ -16,10 +17,12 @@ import { CustomerManagedKey } from "./kms.js";
|
|
|
16
17
|
* costs $1–3/month; a destroyed key would render the surviving table
|
|
17
18
|
* permanently unreadable after its 30-day pending window.
|
|
18
19
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* Constructs reach it through [[forTier]] — the one place the tier's
|
|
21
|
+
* `customerManagedKeys` column is read. Grant-based consumers (DynamoDB,
|
|
22
|
+
* Lambda `environmentEncryption`, ECS Exec) then read `key` directly — the
|
|
23
|
+
* SDK/CDK grants they need ride the resource's own IAM path. Service-side
|
|
24
|
+
* encryptors need a resource-policy admission first: CloudWatch Logs through
|
|
25
|
+
* [[forLogGroups]], SNS service publishers through
|
|
23
26
|
* [[forSnsServicePublishers]]. Each admission is added once per stack.
|
|
24
27
|
*/
|
|
25
28
|
export declare class StackEncryptionKey extends CustomerManagedKey {
|
|
@@ -28,6 +31,16 @@ export declare class StackEncryptionKey extends CustomerManagedKey {
|
|
|
28
31
|
private constructor();
|
|
29
32
|
/** The stack's shared key, created on first use. */
|
|
30
33
|
static of(scope: Construct): StackEncryptionKey;
|
|
34
|
+
/**
|
|
35
|
+
* The stack's shared key when the tier's `customerManagedKeys` column says
|
|
36
|
+
* so, else `undefined` — the one place the column is read. A resource with
|
|
37
|
+
* its own encryption vocabulary (DynamoDB `encryption`, SNS `encryption`)
|
|
38
|
+
* passes the explicit choice as `override`; `undefined` lets the tier
|
|
39
|
+
* decide. The key is only created when the answer is yes, so an opt-out at
|
|
40
|
+
* enterprise mints nothing and an opt-in below enterprise mints the shared
|
|
41
|
+
* key rather than a per-resource one.
|
|
42
|
+
*/
|
|
43
|
+
static forTier(scope: Construct, tier: BackupTier | undefined, override?: boolean): StackEncryptionKey | undefined;
|
|
31
44
|
/**
|
|
32
45
|
* The key, with CloudWatch Logs admitted to it. The CDK `LogGroup` sets
|
|
33
46
|
* `KmsKeyId` but adds no key-policy statement, and without one the
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Stack } from "aws-cdk-lib";
|
|
2
2
|
import { PolicyStatement, ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
|
3
|
+
import { tierDefault } from "../../../utils/backupTierMapping.js";
|
|
3
4
|
import { CustomerManagedKey } from "./kms.js";
|
|
4
5
|
const STACK_ENCRYPTION_KEY_ID = "FjallStackEncryptionKey";
|
|
5
6
|
/**
|
|
@@ -33,10 +34,12 @@ const SNS_SERVICE_PUBLISHERS = [
|
|
|
33
34
|
* costs $1–3/month; a destroyed key would render the surviving table
|
|
34
35
|
* permanently unreadable after its 30-day pending window.
|
|
35
36
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* Constructs reach it through [[forTier]] — the one place the tier's
|
|
38
|
+
* `customerManagedKeys` column is read. Grant-based consumers (DynamoDB,
|
|
39
|
+
* Lambda `environmentEncryption`, ECS Exec) then read `key` directly — the
|
|
40
|
+
* SDK/CDK grants they need ride the resource's own IAM path. Service-side
|
|
41
|
+
* encryptors need a resource-policy admission first: CloudWatch Logs through
|
|
42
|
+
* [[forLogGroups]], SNS service publishers through
|
|
40
43
|
* [[forSnsServicePublishers]]. Each admission is added once per stack.
|
|
41
44
|
*/
|
|
42
45
|
export class StackEncryptionKey extends CustomerManagedKey {
|
|
@@ -59,6 +62,19 @@ export class StackEncryptionKey extends CustomerManagedKey {
|
|
|
59
62
|
return existing;
|
|
60
63
|
return new StackEncryptionKey(stack);
|
|
61
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* The stack's shared key when the tier's `customerManagedKeys` column says
|
|
67
|
+
* so, else `undefined` — the one place the column is read. A resource with
|
|
68
|
+
* its own encryption vocabulary (DynamoDB `encryption`, SNS `encryption`)
|
|
69
|
+
* passes the explicit choice as `override`; `undefined` lets the tier
|
|
70
|
+
* decide. The key is only created when the answer is yes, so an opt-out at
|
|
71
|
+
* enterprise mints nothing and an opt-in below enterprise mints the shared
|
|
72
|
+
* key rather than a per-resource one.
|
|
73
|
+
*/
|
|
74
|
+
static forTier(scope, tier, override) {
|
|
75
|
+
const onStackKey = override ?? tierDefault(tier, "customerManagedKeys");
|
|
76
|
+
return onStackKey ? StackEncryptionKey.of(scope) : undefined;
|
|
77
|
+
}
|
|
62
78
|
/**
|
|
63
79
|
* The key, with CloudWatch Logs admitted to it. The CDK `LogGroup` sets
|
|
64
80
|
* `KmsKeyId` but adds no key-policy statement, and without one the
|
|
@@ -5,9 +5,8 @@ import { type Construct } from "constructs";
|
|
|
5
5
|
*
|
|
6
6
|
* Applies one default: an explicit, stack-shared log group for the deployment
|
|
7
7
|
* handler lambda, so it is CloudFormation-managed and tagged rather than
|
|
8
|
-
* service-auto-created. Per
|
|
9
|
-
*
|
|
10
|
-
* `patterns/infrastructure-wrapper-routing-pattern.md § A3`, `lib/{config,
|
|
8
|
+
* service-auto-created. Per the Wrapper Routing Discipline rule (generator
|
|
9
|
+
* standards) and the infrastructure wrapper-routing pattern (§ A3), `lib/{config,
|
|
11
10
|
* patterns}/` MUST instantiate this class rather than the raw CDK construct.
|
|
12
11
|
*/
|
|
13
12
|
export declare class BucketDeployment extends CdkBucketDeployment {
|
|
@@ -5,9 +5,8 @@ import { stackSingletonLogGroup } from "../logging/singletonLogGroup.js";
|
|
|
5
5
|
*
|
|
6
6
|
* Applies one default: an explicit, stack-shared log group for the deployment
|
|
7
7
|
* handler lambda, so it is CloudFormation-managed and tagged rather than
|
|
8
|
-
* service-auto-created. Per
|
|
9
|
-
*
|
|
10
|
-
* `patterns/infrastructure-wrapper-routing-pattern.md § A3`, `lib/{config,
|
|
8
|
+
* service-auto-created. Per the Wrapper Routing Discipline rule (generator
|
|
9
|
+
* standards) and the infrastructure wrapper-routing pattern (§ A3), `lib/{config,
|
|
11
10
|
* patterns}/` MUST instantiate this class rather than the raw CDK construct.
|
|
12
11
|
*/
|
|
13
12
|
export class BucketDeployment extends CdkBucketDeployment {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { type S3VersioningExemption } from "@fjall/util/aws";
|
|
1
2
|
import { Bucket, type BucketProps } from "aws-cdk-lib/aws-s3";
|
|
2
3
|
import { type Construct } from "constructs";
|
|
3
4
|
import { type BackupTier } from "../../../utils/backupTierMapping.js";
|
|
4
|
-
export { SDK_PRE_EMPTY_TAG_KEY } from "@fjall/util/aws";
|
|
5
|
+
export { S3_VERSIONING_EXEMPT_TAG_KEY, SDK_PRE_EMPTY_TAG_KEY } from "@fjall/util/aws";
|
|
5
6
|
/**
|
|
6
7
|
* Days a noncurrent object version is retained before lifecycle expiry.
|
|
7
8
|
* The wrapper pairs this automatically when it enables versioning; constructs
|
|
@@ -59,6 +60,18 @@ export interface S3BucketProps extends BucketProps {
|
|
|
59
60
|
* direct `new S3Bucket` construction needs to pass it.
|
|
60
61
|
*/
|
|
61
62
|
appBackupTier?: BackupTier;
|
|
63
|
+
/**
|
|
64
|
+
* Why this bucket runs unversioned by design. Stamped as the
|
|
65
|
+
* `fjall:s3:versioning-exempt` tag only when versioning resolves off — a
|
|
66
|
+
* bucket that versions after all (an explicit `versioned: true`, or a tier
|
|
67
|
+
* that turns it on) has nothing to exempt — so posture lands the
|
|
68
|
+
* `S3_VERSIONING_DISABLED` finding `mitigated` rather than `open` on
|
|
69
|
+
* buckets whose contents are rebuilt, not kept. Constructs pass it for the
|
|
70
|
+
* buckets they own (Payload build output and ISR cache, ClickHouse cold
|
|
71
|
+
* tier, the stack access-log bucket); an author may pass it for a
|
|
72
|
+
* disposable bucket of their own.
|
|
73
|
+
*/
|
|
74
|
+
versioningExemption?: S3VersioningExemption;
|
|
62
75
|
publicReadAccess?: boolean;
|
|
63
76
|
websiteHosting?: WebsiteHostingConfig;
|
|
64
77
|
/**
|