@fjall/components-infrastructure 31.0.0 → 33.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 +1 -1
- package/dist/lib/config/audit.js +2 -1
- package/dist/lib/patterns/aws/buildkite/pins.js +1 -2
- package/dist/lib/patterns/aws/cdn.d.ts +14 -0
- package/dist/lib/patterns/aws/cdn.js +31 -3
- package/dist/lib/patterns/aws/clickhouseDatabase.d.ts +8 -0
- package/dist/lib/patterns/aws/clickhouseDatabase.js +15 -2
- package/dist/lib/patterns/aws/compute.js +4 -2
- package/dist/lib/patterns/aws/computeEcs.d.ts +7 -0
- package/dist/lib/patterns/aws/computeEcs.js +26 -3
- package/dist/lib/patterns/aws/computeEcsTypes.d.ts +25 -2
- package/dist/lib/patterns/aws/computeLambda.d.ts +12 -0
- package/dist/lib/patterns/aws/computeLambda.js +2 -0
- package/dist/lib/patterns/aws/computePropApplicability.js +2 -0
- package/dist/lib/patterns/aws/database.d.ts +14 -0
- package/dist/lib/patterns/aws/database.js +16 -6
- package/dist/lib/patterns/aws/fivetranProxy.d.ts +0 -1
- package/dist/lib/patterns/aws/fivetranProxy.js +6 -1
- package/dist/lib/patterns/aws/interfaces/pattern.d.ts +7 -7
- package/dist/lib/patterns/aws/messaging.js +4 -1
- package/dist/lib/patterns/aws/patternDomain.d.ts +1 -1
- package/dist/lib/patterns/aws/patternDomain.js +1 -1
- package/dist/lib/patterns/aws/payload.js +1 -3
- package/dist/lib/patterns/aws/staticSite.d.ts +6 -6
- package/dist/lib/patterns/aws/staticSite.js +9 -6
- package/dist/lib/resources/aws/cdn/cloudFront.d.ts +2 -0
- package/dist/lib/resources/aws/cdn/cloudFront.js +1 -0
- package/dist/lib/resources/aws/compute/applicationLoadBalancer.d.ts +10 -0
- package/dist/lib/resources/aws/compute/applicationLoadBalancer.js +7 -1
- package/dist/lib/resources/aws/compute/ecs.d.ts +11 -0
- package/dist/lib/resources/aws/compute/ecs.js +51 -6
- package/dist/lib/resources/aws/compute/ecsContext.d.ts +10 -1
- package/dist/lib/resources/aws/compute/ecsNetworking.js +3 -1
- package/dist/lib/resources/aws/compute/ecsTaskDefinition.js +4 -1
- package/dist/lib/resources/aws/compute/ecsTypes.d.ts +31 -8
- package/dist/lib/resources/aws/compute/lambda.d.ts +18 -1
- package/dist/lib/resources/aws/compute/lambda.js +21 -3
- package/dist/lib/resources/aws/database/dynamodb.d.ts +19 -0
- package/dist/lib/resources/aws/database/dynamodb.js +13 -2
- package/dist/lib/resources/aws/database/rdsAurora.d.ts +11 -0
- package/dist/lib/resources/aws/database/rdsAurora.js +24 -3
- package/dist/lib/resources/aws/database/rdsAuroraGlobal.d.ts +11 -1
- package/dist/lib/resources/aws/database/rdsAuroraGlobal.js +1 -0
- package/dist/lib/resources/aws/database/rdsDefaults.d.ts +0 -2
- package/dist/lib/resources/aws/database/rdsDefaults.js +0 -2
- package/dist/lib/resources/aws/database/rdsInstance.d.ts +11 -0
- package/dist/lib/resources/aws/database/rdsInstance.js +19 -4
- package/dist/lib/resources/aws/logging/accessLogBucket.d.ts +22 -0
- package/dist/lib/resources/aws/logging/accessLogBucket.js +41 -0
- package/dist/lib/resources/aws/logging/index.d.ts +1 -0
- package/dist/lib/resources/aws/logging/index.js +1 -0
- package/dist/lib/resources/aws/messaging/sns.d.ts +14 -5
- package/dist/lib/resources/aws/messaging/sns.js +7 -1
- package/dist/lib/resources/aws/secrets/index.d.ts +1 -0
- package/dist/lib/resources/aws/secrets/index.js +1 -0
- package/dist/lib/resources/aws/secrets/stackEncryptionKey.d.ts +47 -0
- package/dist/lib/resources/aws/secrets/stackEncryptionKey.js +114 -0
- package/dist/lib/utils/backupTierMapping.d.ts +10 -2
- package/dist/lib/utils/backupTierMapping.js +3 -1
- package/package.json +4 -4
|
@@ -4,8 +4,6 @@ export const RDS_DEFAULTS = Object.freeze({
|
|
|
4
4
|
PREFERRED_MAINTENANCE_WINDOW: "Sat:12:30-Sat:20:30",
|
|
5
5
|
/** 1 minute — balances monitoring granularity against CloudWatch costs */
|
|
6
6
|
MONITORING_INTERVAL: Duration.minutes(1),
|
|
7
|
-
/** Default backup retention in days — applied uniformly across Aurora and RDS Instance */
|
|
8
|
-
BACKUP_RETENTION_DEFAULT_DAYS: 14,
|
|
9
7
|
/** Default instance type — primary and read-replica must stay in lockstep */
|
|
10
8
|
DEFAULT_INSTANCE_TYPE: "t4g.large",
|
|
11
9
|
/** Default storage autoscaling ceiling (GiB) — applied to primary and replica */
|
|
@@ -7,6 +7,7 @@ import { SecurityGroup } from "../networking/securityGroup.js";
|
|
|
7
7
|
import { Secret } from "../secrets/index.js";
|
|
8
8
|
import { type StackBuilder } from "../base/awsStack.js";
|
|
9
9
|
import { type EngineConfig, type ProxyConfig, type ReadReplicaConfig, type CredentialsConfig, type EncryptionConfig, type DatabaseInsightsConfig, type SnapshotTarget } from "../../../utils/databaseTypes.js";
|
|
10
|
+
import { type BackupTier } from "../../../utils/backupTierMapping.js";
|
|
10
11
|
import type { ITopic } from "aws-cdk-lib/aws-sns";
|
|
11
12
|
import { type RdsAlarmThresholds } from "../monitoring/index.js";
|
|
12
13
|
interface RdsProps {
|
|
@@ -17,7 +18,17 @@ interface RdsProps {
|
|
|
17
18
|
instanceType?: string;
|
|
18
19
|
maxAllocatedStorage?: number;
|
|
19
20
|
allocatedStorage?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Automated backup retention. Defaults to the app backup tier's column
|
|
23
|
+
* (`tierDefault(appBackupTier, "rdsBackupRetentionDays")`).
|
|
24
|
+
*/
|
|
20
25
|
backupRetention?: Duration;
|
|
26
|
+
/**
|
|
27
|
+
* The owning app's backup tier (`App.getApp({ backup: { tier } })`), which
|
|
28
|
+
* the retention default follows. `DatabaseFactory` fills it from the App;
|
|
29
|
+
* pass it only for direct `new RdsInstance(...)` construction.
|
|
30
|
+
*/
|
|
31
|
+
appBackupTier?: BackupTier;
|
|
21
32
|
preferredBackupWindow?: string;
|
|
22
33
|
clusterIdentifier?: string;
|
|
23
34
|
monitoringInterval?: Duration;
|
|
@@ -8,11 +8,12 @@ import { Construct } from "constructs";
|
|
|
8
8
|
import { SecurityGroup } from "../networking/securityGroup.js";
|
|
9
9
|
import { CustomerManagedKey, Secret } from "../secrets/index.js";
|
|
10
10
|
import { CustomResource } from "../utilities/customResource.js";
|
|
11
|
-
import { getDatabaseInsightsRetention } from "./index.js";
|
|
11
|
+
import { getCdkDatabaseInsightsMode, getDatabaseInsightsRetention } from "./index.js";
|
|
12
12
|
import { RDS_DEFAULTS } from "./rdsDefaults.js";
|
|
13
13
|
import { DEFAULT_POSTGRES_ENGINE_CONFIG, resolveDatabaseInsights, resolveStorageEncryptionKey, resolvePerformanceInsightsKey, addMultiUserSecretRotation, warnIfSnapshotUsernameAssumed } from "./rdsHelpers.js";
|
|
14
14
|
import { resolveSecretRotation, isCMKRequested } from "../../../utils/databaseTypes.js";
|
|
15
15
|
import { ResourceNaming } from "../../../utils/resourceNaming.js";
|
|
16
|
+
import { tierDefault } from "../../../utils/backupTierMapping.js";
|
|
16
17
|
import { addProxyCfnOutput } from "./rdsProxyOutput.js";
|
|
17
18
|
import { createRdsAlarms } from "../monitoring/index.js";
|
|
18
19
|
export class RdsInstance extends Construct {
|
|
@@ -105,10 +106,15 @@ export class RdsInstance extends Construct {
|
|
|
105
106
|
const storageEncryptionKey = resolveStorageEncryptionKey(this, this.databaseNameValue, props.encryption?.storageKey);
|
|
106
107
|
const { piEnabled, piConfig } = resolveDatabaseInsights(props.databaseInsights);
|
|
107
108
|
const performanceInsightsEncryptionKey = resolvePerformanceInsightsKey(this, this.databaseNameValue, piEnabled, piConfig?.encryptionKey);
|
|
108
|
-
|
|
109
|
+
// The tier owns the Database Insights mode; an explicit `mode` wins.
|
|
110
|
+
const diMode = piConfig?.mode ??
|
|
111
|
+
tierDefault(props.appBackupTier, "databaseInsightsMode");
|
|
109
112
|
const performanceInsightsRetention = piEnabled
|
|
110
113
|
? getDatabaseInsightsRetention(diMode)
|
|
111
114
|
: undefined;
|
|
115
|
+
// Advanced mode is emitted only when it applies, so a standard template
|
|
116
|
+
// stays byte-identical (CDK would otherwise infer Performance Insights on).
|
|
117
|
+
const advancedInsights = piEnabled && diMode === "advanced";
|
|
112
118
|
const engine = props.engine ||
|
|
113
119
|
DatabaseInstanceEngine.postgres({
|
|
114
120
|
version: PostgresEngineVersion.VER_17_9
|
|
@@ -123,7 +129,7 @@ export class RdsInstance extends Construct {
|
|
|
123
129
|
? SubnetType.PUBLIC
|
|
124
130
|
: SubnetType.PRIVATE_WITH_EGRESS;
|
|
125
131
|
const backupRetention = props.backupRetention ??
|
|
126
|
-
Duration.days(
|
|
132
|
+
Duration.days(tierDefault(props.appBackupTier, "rdsBackupRetentionDays"));
|
|
127
133
|
// toDays() throws on a fractional Duration — same whole-days contract the
|
|
128
134
|
// L2 DatabaseInstance enforces, surfaced at synth either way.
|
|
129
135
|
this.backupRetentionDaysValue = backupRetention.toDays();
|
|
@@ -150,6 +156,9 @@ export class RdsInstance extends Construct {
|
|
|
150
156
|
enablePerformanceInsights: piEnabled,
|
|
151
157
|
performanceInsightEncryptionKey: performanceInsightsEncryptionKey,
|
|
152
158
|
performanceInsightRetention: performanceInsightsRetention,
|
|
159
|
+
...(advancedInsights && {
|
|
160
|
+
databaseInsightsMode: getCdkDatabaseInsightsMode(diMode)
|
|
161
|
+
}),
|
|
153
162
|
instanceIdentifier: ResourceNaming.dbInstanceId(this.constructId),
|
|
154
163
|
instanceType: new InstanceType(props.instanceType ?? RDS_DEFAULTS.DEFAULT_INSTANCE_TYPE),
|
|
155
164
|
maxAllocatedStorage: props.maxAllocatedStorage ??
|
|
@@ -243,6 +252,9 @@ export class RdsInstance extends Construct {
|
|
|
243
252
|
props.instanceType ??
|
|
244
253
|
RDS_DEFAULTS.DEFAULT_INSTANCE_TYPE;
|
|
245
254
|
const { piEnabled, piConfig } = resolveDatabaseInsights(props.databaseInsights);
|
|
255
|
+
const diMode = piConfig?.mode ??
|
|
256
|
+
tierDefault(props.appBackupTier, "databaseInsightsMode");
|
|
257
|
+
const advancedInsights = piEnabled && diMode === "advanced";
|
|
246
258
|
const readReplicaPerformanceInsightsKey = piEnabled && isCMKRequested(piConfig?.encryptionKey)
|
|
247
259
|
? new CustomerManagedKey(this, `${this.databaseNameValue}ReadReplicaReaderInsightsKey`, {
|
|
248
260
|
aliasName: ResourceNaming.readReplicaInsightsKeyAlias(this.databaseNameValue),
|
|
@@ -326,8 +338,11 @@ exports.handler = async (event) => {
|
|
|
326
338
|
enablePerformanceInsights: piEnabled,
|
|
327
339
|
performanceInsightEncryptionKey: readReplicaPerformanceInsightsKey,
|
|
328
340
|
performanceInsightRetention: piEnabled
|
|
329
|
-
? getDatabaseInsightsRetention(
|
|
341
|
+
? getDatabaseInsightsRetention(diMode)
|
|
330
342
|
: undefined,
|
|
343
|
+
...(advancedInsights && {
|
|
344
|
+
databaseInsightsMode: getCdkDatabaseInsightsMode(diMode)
|
|
345
|
+
}),
|
|
331
346
|
instanceIdentifier: ResourceNaming.readReplicaId(this.constructId),
|
|
332
347
|
instanceType: new InstanceType(replicaInstanceType),
|
|
333
348
|
availabilityZone: replicaConfig.availabilityZone,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Construct } from "constructs";
|
|
2
|
+
import { S3Bucket } from "../storage/s3.js";
|
|
3
|
+
/**
|
|
4
|
+
* How long delivered access logs stay before the lifecycle rule expires
|
|
5
|
+
* them. Access logs are high-volume and low-value past the incident window;
|
|
6
|
+
* an unexpired log bucket is the one S3 cost that grows without bound.
|
|
7
|
+
*/
|
|
8
|
+
export declare const ACCESS_LOG_RETENTION_DAYS = 90;
|
|
9
|
+
/**
|
|
10
|
+
* The one bucket an enterprise-tier stack delivers its access logs to —
|
|
11
|
+
* application load balancer access logs (`alb/<name>/`) and CloudFront
|
|
12
|
+
* standard logs (`cloudfront/<id>/`) share it under distinct prefixes.
|
|
13
|
+
*
|
|
14
|
+
* Its shape is dictated by the two delivery services and deliberately NOT
|
|
15
|
+
* tier-aware: ELB log delivery requires SSE-S3 (it cannot write to a
|
|
16
|
+
* KMS-encrypted bucket), CloudFront standard logging requires ACLs enabled
|
|
17
|
+
* (`OBJECT_WRITER` ownership, the CDK's own logging-bucket shape), and
|
|
18
|
+
* versioning would only multiply expired log objects. The bucket takes the
|
|
19
|
+
* env-aware removal policy like every other bucket: production RETAINs its
|
|
20
|
+
* audit evidence, non-production DESTROYs.
|
|
21
|
+
*/
|
|
22
|
+
export declare function stackAccessLogBucket(scope: Construct): S3Bucket;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Duration, Stack } from "aws-cdk-lib";
|
|
2
|
+
import { BucketEncryption, ObjectOwnership } from "aws-cdk-lib/aws-s3";
|
|
3
|
+
import { S3Bucket } from "../storage/s3.js";
|
|
4
|
+
const STACK_ACCESS_LOG_BUCKET_ID = "FjallAccessLogBucket";
|
|
5
|
+
/**
|
|
6
|
+
* How long delivered access logs stay before the lifecycle rule expires
|
|
7
|
+
* them. Access logs are high-volume and low-value past the incident window;
|
|
8
|
+
* an unexpired log bucket is the one S3 cost that grows without bound.
|
|
9
|
+
*/
|
|
10
|
+
export const ACCESS_LOG_RETENTION_DAYS = 90;
|
|
11
|
+
/**
|
|
12
|
+
* The one bucket an enterprise-tier stack delivers its access logs to —
|
|
13
|
+
* application load balancer access logs (`alb/<name>/`) and CloudFront
|
|
14
|
+
* standard logs (`cloudfront/<id>/`) share it under distinct prefixes.
|
|
15
|
+
*
|
|
16
|
+
* Its shape is dictated by the two delivery services and deliberately NOT
|
|
17
|
+
* tier-aware: ELB log delivery requires SSE-S3 (it cannot write to a
|
|
18
|
+
* KMS-encrypted bucket), CloudFront standard logging requires ACLs enabled
|
|
19
|
+
* (`OBJECT_WRITER` ownership, the CDK's own logging-bucket shape), and
|
|
20
|
+
* versioning would only multiply expired log objects. The bucket takes the
|
|
21
|
+
* env-aware removal policy like every other bucket: production RETAINs its
|
|
22
|
+
* audit evidence, non-production DESTROYs.
|
|
23
|
+
*/
|
|
24
|
+
export function stackAccessLogBucket(scope) {
|
|
25
|
+
const stack = Stack.of(scope);
|
|
26
|
+
const existing = stack.node.tryFindChild(STACK_ACCESS_LOG_BUCKET_ID);
|
|
27
|
+
if (existing !== undefined)
|
|
28
|
+
return existing;
|
|
29
|
+
return new S3Bucket(stack, STACK_ACCESS_LOG_BUCKET_ID, {
|
|
30
|
+
encryption: BucketEncryption.S3_MANAGED,
|
|
31
|
+
objectOwnership: ObjectOwnership.OBJECT_WRITER,
|
|
32
|
+
versioned: false,
|
|
33
|
+
lifecycleRules: [
|
|
34
|
+
{
|
|
35
|
+
id: "ExpireAccessLogs",
|
|
36
|
+
expiration: Duration.days(ACCESS_LOG_RETENTION_DAYS),
|
|
37
|
+
enabled: true
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -2,6 +2,7 @@ import { Construct } from "constructs";
|
|
|
2
2
|
import { type IKey } from "aws-cdk-lib/aws-kms";
|
|
3
3
|
import { type ITopic } from "aws-cdk-lib/aws-sns";
|
|
4
4
|
import { type IGrantable, type Grant } from "aws-cdk-lib/aws-iam";
|
|
5
|
+
import { type BackupTier } from "../../../utils/backupTierMapping.js";
|
|
5
6
|
import { type RemovalPolicyString } from "./utils.js";
|
|
6
7
|
export interface SNSTopicProps {
|
|
7
8
|
topicName?: string;
|
|
@@ -23,11 +24,13 @@ export interface SNSTopicProps {
|
|
|
23
24
|
*/
|
|
24
25
|
signatureVersion?: "1" | "2";
|
|
25
26
|
/**
|
|
26
|
-
* KMS key for server-side encryption at rest. Absent ⇒
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
27
|
+
* KMS key for server-side encryption at rest. Absent ⇒ the app backup
|
|
28
|
+
* tier decides: enterprise encrypts with the stack's shared
|
|
29
|
+
* [[StackEncryptionKey]] (its SNS service publishers admitted); every other
|
|
30
|
+
* tier stays unencrypted — the accessible default (a customer-managed key
|
|
31
|
+
* costs $1–3/month per account-region once annual rotation accrues), so
|
|
32
|
+
* the SNS_TOPIC_NOT_ENCRYPTED posture finding stands as the signpost to
|
|
33
|
+
* the tier rather than being silently pre-paid.
|
|
31
34
|
*
|
|
32
35
|
* Every AWS service principal that publishes to the topic needs
|
|
33
36
|
* `kms:Decrypt` + `kms:GenerateDataKey*` on this key's RESOURCE policy or
|
|
@@ -35,6 +38,12 @@ export interface SNSTopicProps {
|
|
|
35
38
|
* used for service publishers precisely because its policy is immutable.
|
|
36
39
|
*/
|
|
37
40
|
masterKey?: IKey;
|
|
41
|
+
/**
|
|
42
|
+
* The owning app's backup tier, which the encryption default follows.
|
|
43
|
+
* `MessagingFactory` fills it from the App; pass it only for direct
|
|
44
|
+
* `new SNSTopic(...)` construction.
|
|
45
|
+
*/
|
|
46
|
+
appBackupTier?: BackupTier;
|
|
38
47
|
}
|
|
39
48
|
export declare class SNSTopic extends Construct {
|
|
40
49
|
readonly id: string;
|
|
@@ -1,6 +1,8 @@
|
|
|
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
|
+
import { StackEncryptionKey } from "../secrets/stackEncryptionKey.js";
|
|
4
6
|
import { toRemovalPolicy } from "./utils.js";
|
|
5
7
|
export class SNSTopic extends Construct {
|
|
6
8
|
id;
|
|
@@ -9,6 +11,10 @@ export class SNSTopic extends Construct {
|
|
|
9
11
|
super(scope, id);
|
|
10
12
|
this.id = id;
|
|
11
13
|
const isFifo = props.fifo === true;
|
|
14
|
+
const masterKey = props.masterKey ??
|
|
15
|
+
(tierDefault(props.appBackupTier, "customerManagedKeys")
|
|
16
|
+
? StackEncryptionKey.of(this).forSnsServicePublishers()
|
|
17
|
+
: undefined);
|
|
12
18
|
const topicName = props.topicName
|
|
13
19
|
? isFifo
|
|
14
20
|
? `${props.topicName}.fifo`
|
|
@@ -29,7 +35,7 @@ export class SNSTopic extends Construct {
|
|
|
29
35
|
// (SharedAlarmTopic carries the cloudwatch.amazonaws.com one).
|
|
30
36
|
// Same-account IAM publishers are unaffected: identity policies suffice.
|
|
31
37
|
enforceSSL: true,
|
|
32
|
-
masterKey
|
|
38
|
+
masterKey
|
|
33
39
|
});
|
|
34
40
|
// An SNS topic is a transient fan-out medium: it holds no durable state
|
|
35
41
|
// (subscriptions are re-created by the next deploy), so the wrapper defaults
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { IKey } from "aws-cdk-lib/aws-kms";
|
|
2
|
+
import type { Construct } from "constructs";
|
|
3
|
+
import { CustomerManagedKey } from "./kms.js";
|
|
4
|
+
/**
|
|
5
|
+
* The one customer-managed KMS key an enterprise-tier stack encrypts its
|
|
6
|
+
* tier-owned resources with: DynamoDB tables, SNS topics, Lambda environment
|
|
7
|
+
* variables, ECS Exec sessions and the log groups the tier-aware constructs
|
|
8
|
+
* create. One key per stack rather than one per resource — a key is billed
|
|
9
|
+
* per month whether it protects one table or twenty, its policy is the one
|
|
10
|
+
* place service principals are admitted, and a posture scan sees one
|
|
11
|
+
* purposeful key rather than a sprawl of orphans after a destroy.
|
|
12
|
+
*
|
|
13
|
+
* `protects: "outlives-stack"`: a DynamoDB table RETAINs by default in every
|
|
14
|
+
* environment and a production log group RETAINs under D17, so at least one
|
|
15
|
+
* encrypted resource can survive the stack that created it. A retained key
|
|
16
|
+
* costs $1–3/month; a destroyed key would render the surviving table
|
|
17
|
+
* permanently unreadable after its 30-day pending window.
|
|
18
|
+
*
|
|
19
|
+
* Grant-based consumers (DynamoDB, Lambda `environmentEncryption`, ECS Exec)
|
|
20
|
+
* read `key` directly — the SDK/CDK grants they need ride the resource's own
|
|
21
|
+
* IAM path. Service-side encryptors need a resource-policy admission first:
|
|
22
|
+
* CloudWatch Logs through [[forLogGroups]], SNS service publishers through
|
|
23
|
+
* [[forSnsServicePublishers]]. Each admission is added once per stack.
|
|
24
|
+
*/
|
|
25
|
+
export declare class StackEncryptionKey extends CustomerManagedKey {
|
|
26
|
+
private logsAdmitted;
|
|
27
|
+
private snsPublishersAdmitted;
|
|
28
|
+
private constructor();
|
|
29
|
+
/** The stack's shared key, created on first use. */
|
|
30
|
+
static of(scope: Construct): StackEncryptionKey;
|
|
31
|
+
/**
|
|
32
|
+
* The key, with CloudWatch Logs admitted to it. The CDK `LogGroup` sets
|
|
33
|
+
* `KmsKeyId` but adds no key-policy statement, and without one the
|
|
34
|
+
* service cannot encrypt the group and the stack fails to create. The
|
|
35
|
+
* encryption-context condition confines the admission to log groups in
|
|
36
|
+
* this account and region (the AWS-documented shape).
|
|
37
|
+
*/
|
|
38
|
+
forLogGroups(): IKey;
|
|
39
|
+
/**
|
|
40
|
+
* The key, with the SNS-publishing AWS services admitted to it
|
|
41
|
+
* ([[SNS_SERVICE_PUBLISHERS]]). Without the admission their publishes to
|
|
42
|
+
* an encrypted topic fail silently — the AWS-managed `aws/sns` key cannot
|
|
43
|
+
* be used for service publishers precisely because its policy is
|
|
44
|
+
* immutable.
|
|
45
|
+
*/
|
|
46
|
+
forSnsServicePublishers(): IKey;
|
|
47
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Stack } from "aws-cdk-lib";
|
|
2
|
+
import { PolicyStatement, ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
|
3
|
+
import { CustomerManagedKey } from "./kms.js";
|
|
4
|
+
const STACK_ENCRYPTION_KEY_ID = "FjallStackEncryptionKey";
|
|
5
|
+
/**
|
|
6
|
+
* AWS services that publish to an application's SNS topics on the app's
|
|
7
|
+
* behalf and therefore encrypt messages through the topic's key: EventBridge
|
|
8
|
+
* rule targets, CloudWatch alarm actions and S3 event notifications. All
|
|
9
|
+
* three need `kms:Decrypt` + `kms:GenerateDataKey*` on the key's RESOURCE
|
|
10
|
+
* policy (identity policies cannot admit a service principal), and none of
|
|
11
|
+
* them can be narrowed with `aws:SourceAccount` / `aws:SourceArn`: EventBridge
|
|
12
|
+
* does not support the condition keys for encrypted topics, and CloudWatch
|
|
13
|
+
* does not reliably pass them on this path and fails silently when they do
|
|
14
|
+
* not match (alarms page nobody) — the SharedAlarmTopic precedent.
|
|
15
|
+
*/
|
|
16
|
+
const SNS_SERVICE_PUBLISHERS = [
|
|
17
|
+
"events.amazonaws.com",
|
|
18
|
+
"cloudwatch.amazonaws.com",
|
|
19
|
+
"s3.amazonaws.com"
|
|
20
|
+
];
|
|
21
|
+
/**
|
|
22
|
+
* The one customer-managed KMS key an enterprise-tier stack encrypts its
|
|
23
|
+
* tier-owned resources with: DynamoDB tables, SNS topics, Lambda environment
|
|
24
|
+
* variables, ECS Exec sessions and the log groups the tier-aware constructs
|
|
25
|
+
* create. One key per stack rather than one per resource — a key is billed
|
|
26
|
+
* per month whether it protects one table or twenty, its policy is the one
|
|
27
|
+
* place service principals are admitted, and a posture scan sees one
|
|
28
|
+
* purposeful key rather than a sprawl of orphans after a destroy.
|
|
29
|
+
*
|
|
30
|
+
* `protects: "outlives-stack"`: a DynamoDB table RETAINs by default in every
|
|
31
|
+
* environment and a production log group RETAINs under D17, so at least one
|
|
32
|
+
* encrypted resource can survive the stack that created it. A retained key
|
|
33
|
+
* costs $1–3/month; a destroyed key would render the surviving table
|
|
34
|
+
* permanently unreadable after its 30-day pending window.
|
|
35
|
+
*
|
|
36
|
+
* Grant-based consumers (DynamoDB, Lambda `environmentEncryption`, ECS Exec)
|
|
37
|
+
* read `key` directly — the SDK/CDK grants they need ride the resource's own
|
|
38
|
+
* IAM path. Service-side encryptors need a resource-policy admission first:
|
|
39
|
+
* CloudWatch Logs through [[forLogGroups]], SNS service publishers through
|
|
40
|
+
* [[forSnsServicePublishers]]. Each admission is added once per stack.
|
|
41
|
+
*/
|
|
42
|
+
export class StackEncryptionKey extends CustomerManagedKey {
|
|
43
|
+
logsAdmitted = false;
|
|
44
|
+
snsPublishersAdmitted = false;
|
|
45
|
+
constructor(stack) {
|
|
46
|
+
super(stack, STACK_ENCRYPTION_KEY_ID, {
|
|
47
|
+
description: `Encrypts the enterprise-tier resources of ${stack.stackName} (DynamoDB tables, SNS topics, Lambda environment, ECS Exec sessions, log groups)`,
|
|
48
|
+
// KMS alias names are unique per account-region, so the stack name is
|
|
49
|
+
// the namespace — every stack in the account carries its own key.
|
|
50
|
+
aliasName: `cmk/${stack.stackName}/${STACK_ENCRYPTION_KEY_ID}`,
|
|
51
|
+
protects: "outlives-stack"
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** The stack's shared key, created on first use. */
|
|
55
|
+
static of(scope) {
|
|
56
|
+
const stack = Stack.of(scope);
|
|
57
|
+
const existing = stack.node.tryFindChild(STACK_ENCRYPTION_KEY_ID);
|
|
58
|
+
if (existing !== undefined)
|
|
59
|
+
return existing;
|
|
60
|
+
return new StackEncryptionKey(stack);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The key, with CloudWatch Logs admitted to it. The CDK `LogGroup` sets
|
|
64
|
+
* `KmsKeyId` but adds no key-policy statement, and without one the
|
|
65
|
+
* service cannot encrypt the group and the stack fails to create. The
|
|
66
|
+
* encryption-context condition confines the admission to log groups in
|
|
67
|
+
* this account and region (the AWS-documented shape).
|
|
68
|
+
*/
|
|
69
|
+
forLogGroups() {
|
|
70
|
+
if (!this.logsAdmitted) {
|
|
71
|
+
const stack = Stack.of(this);
|
|
72
|
+
this.key.addToResourcePolicy(new PolicyStatement({
|
|
73
|
+
sid: "AllowCloudWatchLogs",
|
|
74
|
+
principals: [
|
|
75
|
+
new ServicePrincipal(`logs.${stack.region}.amazonaws.com`)
|
|
76
|
+
],
|
|
77
|
+
actions: [
|
|
78
|
+
"kms:Encrypt*",
|
|
79
|
+
"kms:Decrypt*",
|
|
80
|
+
"kms:ReEncrypt*",
|
|
81
|
+
"kms:GenerateDataKey*",
|
|
82
|
+
"kms:Describe*"
|
|
83
|
+
],
|
|
84
|
+
resources: ["*"],
|
|
85
|
+
conditions: {
|
|
86
|
+
ArnLike: {
|
|
87
|
+
"kms:EncryptionContext:aws:logs:arn": `arn:${stack.partition}:logs:${stack.region}:${stack.account}:log-group:*`
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}));
|
|
91
|
+
this.logsAdmitted = true;
|
|
92
|
+
}
|
|
93
|
+
return this.key;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The key, with the SNS-publishing AWS services admitted to it
|
|
97
|
+
* ([[SNS_SERVICE_PUBLISHERS]]). Without the admission their publishes to
|
|
98
|
+
* an encrypted topic fail silently — the AWS-managed `aws/sns` key cannot
|
|
99
|
+
* be used for service publishers precisely because its policy is
|
|
100
|
+
* immutable.
|
|
101
|
+
*/
|
|
102
|
+
forSnsServicePublishers() {
|
|
103
|
+
if (!this.snsPublishersAdmitted) {
|
|
104
|
+
this.key.addToResourcePolicy(new PolicyStatement({
|
|
105
|
+
sid: "AllowSnsServicePublishers",
|
|
106
|
+
principals: SNS_SERVICE_PUBLISHERS.map((service) => new ServicePrincipal(service)),
|
|
107
|
+
actions: ["kms:Decrypt", "kms:GenerateDataKey*"],
|
|
108
|
+
resources: ["*"]
|
|
109
|
+
}));
|
|
110
|
+
this.snsPublishersAdmitted = true;
|
|
111
|
+
}
|
|
112
|
+
return this.key;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
import { BACKUP_TIERS, BACKUP_TIER_DEFAULTS, type BackupTier, type BackupTierDefaults, tierDefault } from "@fjall/util";
|
|
1
2
|
/** Tag key used by AWS Backup plan selection rules */
|
|
2
3
|
export declare const BACKUP_TIER_TAG_KEY = "fjall:disasterRecovery:tier";
|
|
3
|
-
/**
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* The application backup tier vocabulary and its defaults table are owned by
|
|
6
|
+
* `@fjall/util` (`BACKUP_TIERS`, `BACKUP_TIER_DEFAULTS`, `tierDefault`) and
|
|
7
|
+
* shared with the generator (`BACKUP_VAULT_TIERS`) and the CLI's synth
|
|
8
|
+
* expectations, so every system derives from one declaration. Re-exported
|
|
9
|
+
* here so construct code keeps one import path for tier concerns.
|
|
10
|
+
*/
|
|
11
|
+
export type { BackupTier, BackupTierDefaults };
|
|
12
|
+
export { BACKUP_TIERS, BACKUP_TIER_DEFAULTS, tierDefault };
|
|
5
13
|
/**
|
|
6
14
|
* Shared mapping from application backup tier names to AWS Backup plan tag values.
|
|
7
15
|
*
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { BACKUP_TIERS, BACKUP_TIER_DEFAULTS, tierDefault } from "@fjall/util";
|
|
1
2
|
/** Tag key used by AWS Backup plan selection rules */
|
|
2
3
|
export const BACKUP_TIER_TAG_KEY = "fjall:disasterRecovery:tier";
|
|
4
|
+
export { BACKUP_TIERS, BACKUP_TIER_DEFAULTS, tierDefault };
|
|
3
5
|
/**
|
|
4
6
|
* Shared mapping from application backup tier names to AWS Backup plan tag values.
|
|
5
7
|
*
|
|
@@ -22,5 +24,5 @@ export function resolveEffectiveBackupTier(resourceTier, appTier) {
|
|
|
22
24
|
}
|
|
23
25
|
/** Tiers whose S3 buckets version by default. */
|
|
24
26
|
export function shouldAutoVersion(tier) {
|
|
25
|
-
return tier
|
|
27
|
+
return tierDefault(tier, "s3Versioned");
|
|
26
28
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fjall/components-infrastructure",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "33.0.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/fjall-tech/fjall.git",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"clean": "rm -rf ./dist",
|
|
40
40
|
"clean:node": "rm -rf ./node_modules",
|
|
41
41
|
"build:cert-gen-lambda": "node lib/lambda-assets/cert-generator/src/build.mjs",
|
|
42
|
-
"build": "npm run build:cert-gen-lambda && tsc && cp -r lib/layers dist/lib/layers && mkdir -p dist/lib/lambda-assets/cert-generator/asset && cp lib/lambda-assets/cert-generator/asset/index.js dist/lib/lambda-assets/cert-generator/asset/index.js && cp lib/lambda-assets/cert-generator/asset/package.json dist/lib/lambda-assets/cert-generator/asset/package.json && mkdir -p dist/lib/lambda-assets/identity-store-user/asset && cp lib/lambda-assets/identity-store-user/asset/index.js dist/lib/lambda-assets/identity-store-user/asset/index.js && cp lib/lambda-assets/identity-store-user/asset/package.json dist/lib/lambda-assets/identity-store-user/asset/package.json && mkdir -p dist/lib/lambda-assets/static-site-forms/asset && cp lib/lambda-assets/static-site-forms/asset/index.js dist/lib/lambda-assets/static-site-forms/asset/index.js && cp lib/lambda-assets/static-site-forms/asset/package.json dist/lib/lambda-assets/static-site-forms/asset/package.json && cp lib/resources/aws/compute/lifecycleHookLambda.source.cjs dist/lib/resources/aws/compute/lifecycleHookLambda.source.cjs && cp lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs dist/lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs && cp lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs dist/lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs && cp lib/patterns/aws/devSubstrate.waker.source.cjs dist/lib/patterns/aws/devSubstrate.waker.source.cjs && node ../../scripts/stamp-build.mjs",
|
|
42
|
+
"build": "npm run clean && npm run build:cert-gen-lambda && tsc && cp -r lib/layers dist/lib/layers && mkdir -p dist/lib/lambda-assets/cert-generator/asset && cp lib/lambda-assets/cert-generator/asset/index.js dist/lib/lambda-assets/cert-generator/asset/index.js && cp lib/lambda-assets/cert-generator/asset/package.json dist/lib/lambda-assets/cert-generator/asset/package.json && mkdir -p dist/lib/lambda-assets/identity-store-user/asset && cp lib/lambda-assets/identity-store-user/asset/index.js dist/lib/lambda-assets/identity-store-user/asset/index.js && cp lib/lambda-assets/identity-store-user/asset/package.json dist/lib/lambda-assets/identity-store-user/asset/package.json && mkdir -p dist/lib/lambda-assets/static-site-forms/asset && cp lib/lambda-assets/static-site-forms/asset/index.js dist/lib/lambda-assets/static-site-forms/asset/index.js && cp lib/lambda-assets/static-site-forms/asset/package.json dist/lib/lambda-assets/static-site-forms/asset/package.json && cp lib/resources/aws/compute/lifecycleHookLambda.source.cjs dist/lib/resources/aws/compute/lifecycleHookLambda.source.cjs && cp lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs dist/lib/resources/aws/compute/ec2GracefulTerminationLambda.source.cjs && cp lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs dist/lib/resources/aws/compute/persistentDataVolumeLambda.source.cjs && cp lib/patterns/aws/devSubstrate.waker.source.cjs dist/lib/patterns/aws/devSubstrate.waker.source.cjs && node ../../scripts/stamp-build.mjs",
|
|
43
43
|
"prepack": "node ../../scripts/check-dist-freshness.mjs",
|
|
44
44
|
"watch": "tsc -w",
|
|
45
45
|
"watch:only": "tsc -w",
|
|
@@ -78,8 +78,8 @@
|
|
|
78
78
|
},
|
|
79
79
|
"dependencies": {
|
|
80
80
|
"@aws-sdk/client-organizations": "^3.1098.0",
|
|
81
|
-
"@fjall/generator": "^
|
|
82
|
-
"@fjall/util": "^
|
|
81
|
+
"@fjall/generator": "^33.0.0",
|
|
82
|
+
"@fjall/util": "^33.0.0",
|
|
83
83
|
"constructs": "^10.7.2",
|
|
84
84
|
"zod": "^4.4.3"
|
|
85
85
|
},
|