@fjall/components-infrastructure 2.12.0 → 2.14.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/dist/lib/app.d.ts +7 -7
- package/dist/lib/app.js +2 -3
- package/dist/lib/config/aws/accountMonitoringRole.js +2 -1
- package/dist/lib/config/aws/cloudTrail.d.ts +13 -0
- package/dist/lib/config/aws/cloudTrail.js +19 -3
- package/dist/lib/config/aws/disasterRecovery.js +1 -1
- package/dist/lib/config/aws/ecrDefaultImage.js +2 -0
- package/dist/lib/config/aws/organisationTrail.d.ts +16 -0
- package/dist/lib/config/aws/organisationTrail.js +32 -0
- package/dist/lib/config/aws/scpPreset.js +10 -1
- package/dist/lib/patterns/aws/account.d.ts +9 -0
- package/dist/lib/patterns/aws/account.js +29 -6
- package/dist/lib/patterns/aws/organisation.d.ts +9 -0
- package/dist/lib/patterns/aws/organisation.js +51 -5
- package/dist/lib/patterns/aws/storage.d.ts +1 -1
- package/dist/lib/patterns/aws/storage.js +5 -1
- package/dist/lib/resources/aws/logging/cloudTrail.d.ts +48 -1
- package/dist/lib/resources/aws/logging/cloudTrail.js +180 -18
- package/dist/lib/resources/aws/messaging/eventbridge.d.ts +3 -2
- package/dist/lib/resources/aws/messaging/eventbridge.js +2 -2
- package/dist/lib/resources/aws/networking/ipamPool.js +6 -3
- package/dist/lib/resources/aws/networking/serviceDiscovery.d.ts +4 -3
- package/dist/lib/resources/aws/networking/serviceDiscovery.js +2 -3
- package/dist/lib/resources/aws/storage/s3.d.ts +8 -0
- package/dist/lib/resources/aws/storage/s3.js +19 -4
- package/dist/lib/utils/cdkContext.d.ts +11 -0
- package/dist/lib/utils/cdkContext.js +22 -1
- package/dist/lib/utils/env.d.ts +19 -0
- package/dist/lib/utils/env.js +36 -5
- package/dist/lib/utils/getConfig.js +32 -12
- package/dist/lib/utils/orgConfigParser.d.ts +10 -0
- package/dist/lib/utils/orgConfigParser.js +47 -23
- package/dist/lib/utils/removalPolicy.d.ts +15 -0
- package/dist/lib/utils/removalPolicy.js +32 -0
- package/dist/lib/utils/standardTagsAspect.js +2 -1
- package/package.json +4 -4
package/dist/lib/app.d.ts
CHANGED
|
@@ -72,10 +72,11 @@ export interface IAppOptions {
|
|
|
72
72
|
*
|
|
73
73
|
* - `name?` overrides the AWS `EventBus.Name` (defaults to the app name).
|
|
74
74
|
* - `removalPolicy?` overrides the env-resolved default. Default resolves
|
|
75
|
-
* via `
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
75
|
+
* via `envAwareRemovalPolicyDefault()` per D17 (production → RETAIN,
|
|
76
|
+
* other recognised stages → DESTROY, unrecognised values fail synth) —
|
|
77
|
+
* NOT `process.env.NODE_ENV` (which is not set during CDK synth in
|
|
78
|
+
* Fjall's deployment paths). The shape is the normalised string union
|
|
79
|
+
* per D18(d), NOT the raw CDK `RemovalPolicy` enum.
|
|
79
80
|
*/
|
|
80
81
|
eventBus?: {
|
|
81
82
|
name?: string;
|
|
@@ -175,9 +176,8 @@ export declare class App extends CdkApp {
|
|
|
175
176
|
* Buses are recreatable in non-prod; the env-resolved default keeps prod
|
|
176
177
|
* history. The override (`App.getApp({ eventBus: { name?, removalPolicy? } })`)
|
|
177
178
|
* is consulted first; absent override falls back to the app name and the
|
|
178
|
-
* `
|
|
179
|
-
*
|
|
180
|
-
* deployment paths.
|
|
179
|
+
* `envAwareRemovalPolicyDefault()` resolution per D17. NODE_ENV is not
|
|
180
|
+
* consulted — it is not set during CDK synth in Fjall's deployment paths.
|
|
181
181
|
*/
|
|
182
182
|
getEventBus(): EventBusMessaging;
|
|
183
183
|
/**
|
package/dist/lib/app.js
CHANGED
|
@@ -277,9 +277,8 @@ export class App extends CdkApp {
|
|
|
277
277
|
* Buses are recreatable in non-prod; the env-resolved default keeps prod
|
|
278
278
|
* history. The override (`App.getApp({ eventBus: { name?, removalPolicy? } })`)
|
|
279
279
|
* is consulted first; absent override falls back to the app name and the
|
|
280
|
-
* `
|
|
281
|
-
*
|
|
282
|
-
* deployment paths.
|
|
280
|
+
* `envAwareRemovalPolicyDefault()` resolution per D17. NODE_ENV is not
|
|
281
|
+
* consulted — it is not set during CDK synth in Fjall's deployment paths.
|
|
283
282
|
*/
|
|
284
283
|
getEventBus() {
|
|
285
284
|
if (this.defaultEventBus) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ACCOUNT_MONITORING_ROLE_NAME } from "@fjall/util/aws";
|
|
1
2
|
import { CfnOutput } from "aws-cdk-lib";
|
|
2
3
|
import { Role, AccountPrincipal, PolicyStatement, Effect } from "aws-cdk-lib/aws-iam";
|
|
3
4
|
import { Construct } from "constructs";
|
|
@@ -16,7 +17,7 @@ export class AccountMonitoringRole extends Construct {
|
|
|
16
17
|
constructor(scope, id, props) {
|
|
17
18
|
super(scope, id);
|
|
18
19
|
this.role = new Role(this, "Role", {
|
|
19
|
-
roleName:
|
|
20
|
+
roleName: ACCOUNT_MONITORING_ROLE_NAME,
|
|
20
21
|
path: "/",
|
|
21
22
|
assumedBy: new AccountPrincipal(FJALL_PLATFORM_ACCOUNT_ID),
|
|
22
23
|
description: "Cross-account monitoring role for the Fjall platform. Grants read access to CloudWatch, ECS, RDS, S3, Lambda, ALB, Logs, and Cost Explorer.",
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import { Construct } from "constructs";
|
|
2
|
+
import type { AccountTrailState } from "../../utils/cdkContext.js";
|
|
2
3
|
export interface ManagementEventsTrailProps {
|
|
3
4
|
accountId: string;
|
|
4
5
|
region: string;
|
|
6
|
+
/**
|
|
7
|
+
* "active" (default) synthesises trail + bucket + CMK. "draining" omits the
|
|
8
|
+
* trail resource so CloudFormation deletes it, while the bucket + CMK keep
|
|
9
|
+
* their logical IDs and retained history. "removed" never reaches this
|
|
10
|
+
* construct — Account skips construction entirely.
|
|
11
|
+
*/
|
|
12
|
+
lifecycleState?: Exclude<AccountTrailState, "removed">;
|
|
13
|
+
/**
|
|
14
|
+
* Bucket + CMK survive stack deletion. Default true; Account passes false
|
|
15
|
+
* for development environments to avoid retained-orphan churn.
|
|
16
|
+
*/
|
|
17
|
+
retainAuditHistory?: boolean;
|
|
5
18
|
}
|
|
6
19
|
export declare class ManagementEventsTrail extends Construct {
|
|
7
20
|
constructor(scope: Construct, id: string, props: ManagementEventsTrailProps);
|
|
@@ -1,12 +1,28 @@
|
|
|
1
|
+
import { CfnOutput } from "aws-cdk-lib";
|
|
1
2
|
import { Construct } from "constructs";
|
|
3
|
+
import { ACCOUNT_TRAIL_NAME, TRAIL_BUCKET_OUTPUT_KEY, TRAIL_KEY_ARN_OUTPUT_KEY } from "@fjall/util/config";
|
|
2
4
|
import { Trail } from "../../resources/aws/logging/cloudTrail.js";
|
|
3
5
|
export class ManagementEventsTrail extends Construct {
|
|
4
6
|
constructor(scope, id, props) {
|
|
5
7
|
super(scope, id);
|
|
6
|
-
new Trail(this, "ManagementEventsTrail", {
|
|
8
|
+
const trail = new Trail(this, "ManagementEventsTrail", {
|
|
7
9
|
bucketName: `cloudtrail-management-events-${props.accountId}-${props.region}`,
|
|
8
|
-
trailName:
|
|
9
|
-
isMultiRegionTrail: true
|
|
10
|
+
trailName: ACCOUNT_TRAIL_NAME,
|
|
11
|
+
isMultiRegionTrail: true,
|
|
12
|
+
retainAuditHistory: props.retainAuditHistory ?? true,
|
|
13
|
+
omitTrail: props.lifecycleState === "draining"
|
|
14
|
+
});
|
|
15
|
+
new CfnOutput(this, "TrailBucketNameOutput", {
|
|
16
|
+
key: TRAIL_BUCKET_OUTPUT_KEY,
|
|
17
|
+
value: trail.bucket.bucketName,
|
|
18
|
+
exportName: TRAIL_BUCKET_OUTPUT_KEY,
|
|
19
|
+
description: "Per-account CloudTrail bucket, read by the org-trail migration reconciler"
|
|
20
|
+
});
|
|
21
|
+
new CfnOutput(this, "TrailKeyArnOutput", {
|
|
22
|
+
key: TRAIL_KEY_ARN_OUTPUT_KEY,
|
|
23
|
+
value: trail.encryptionKey.key.keyArn,
|
|
24
|
+
exportName: TRAIL_KEY_ARN_OUTPUT_KEY,
|
|
25
|
+
description: "Per-account CloudTrail CMK, read by the org-trail migration reconciler"
|
|
10
26
|
});
|
|
11
27
|
}
|
|
12
28
|
}
|
|
@@ -6,6 +6,7 @@ import { cronSchedule } from "../../resources/aws/messaging/index.js";
|
|
|
6
6
|
import { AccountPrincipal, Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
7
7
|
import { getConfig } from "../../utils/getConfig.js";
|
|
8
8
|
import { toPascalCase } from "../../utils/capitaliseString.js";
|
|
9
|
+
import { BACKUP_VAULT_NAME } from "@fjall/util";
|
|
9
10
|
// Backup retention constants (in days)
|
|
10
11
|
const RETENTION_PERIODS = {
|
|
11
12
|
STANDARD: 90,
|
|
@@ -21,7 +22,6 @@ const BACKUP_WINDOWS = {
|
|
|
21
22
|
COMPLETION: Duration.hours(12),
|
|
22
23
|
COMPLETION_SHORT: Duration.hours(2)
|
|
23
24
|
};
|
|
24
|
-
const BACKUP_VAULT_NAME = "backupVault";
|
|
25
25
|
// Vault lock constants
|
|
26
26
|
const VAULT_LOCK = {
|
|
27
27
|
MIN_RETENTION: Duration.days(365),
|
|
@@ -2,6 +2,7 @@ import { Construct } from "constructs";
|
|
|
2
2
|
import { CodeBuildProject } from "../../resources/aws/utilities/codeBuild.js";
|
|
3
3
|
import { BuildSpec } from "aws-cdk-lib/aws-codebuild";
|
|
4
4
|
import { LogGroup } from "../../resources/aws/logging/logGroup.js";
|
|
5
|
+
import { RetentionDays } from "aws-cdk-lib/aws-logs";
|
|
5
6
|
import { RemovalPolicy, Stack } from "aws-cdk-lib";
|
|
6
7
|
import { Role } from "../../resources/aws/iam/role.js";
|
|
7
8
|
import { PolicyDocument, PolicyStatement, ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
|
@@ -11,6 +12,7 @@ export class EcrDefaultImage extends Construct {
|
|
|
11
12
|
super(scope, id);
|
|
12
13
|
const logGroup = new LogGroup(this, `${id}LogGroup`, {
|
|
13
14
|
logGroupName: `/vpc/codebuild/${id}/`,
|
|
15
|
+
retention: RetentionDays.ONE_MONTH,
|
|
14
16
|
removalPolicy: RemovalPolicy.DESTROY
|
|
15
17
|
});
|
|
16
18
|
const ecrDefaultImageRole = new Role(this, `${id}Role`, {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Construct } from "constructs";
|
|
2
|
+
export interface OrganisationTrailProps {
|
|
3
|
+
orgId: string;
|
|
4
|
+
managementAccountId: string;
|
|
5
|
+
region: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Organisation-wide management-events trail, owned by the management account.
|
|
9
|
+
* Replaces per-account ManagementEventsTrail instances in organisation mode:
|
|
10
|
+
* AWS records every member's management events into the one bucket under
|
|
11
|
+
* AWSLogs/<orgId>/<accountId>/, and members cannot stop or alter the shadow
|
|
12
|
+
* trail AWS materialises for them.
|
|
13
|
+
*/
|
|
14
|
+
export declare class OrganisationTrail extends Construct {
|
|
15
|
+
constructor(scope: Construct, id: string, props: OrganisationTrailProps);
|
|
16
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { CfnOutput } from "aws-cdk-lib";
|
|
2
|
+
import { Construct } from "constructs";
|
|
3
|
+
import { ORG_TRAIL_BUCKET_OUTPUT_KEY, ORGANISATION_TRAIL_NAME } from "@fjall/util/config";
|
|
4
|
+
import { Trail } from "../../resources/aws/logging/cloudTrail.js";
|
|
5
|
+
/**
|
|
6
|
+
* Organisation-wide management-events trail, owned by the management account.
|
|
7
|
+
* Replaces per-account ManagementEventsTrail instances in organisation mode:
|
|
8
|
+
* AWS records every member's management events into the one bucket under
|
|
9
|
+
* AWSLogs/<orgId>/<accountId>/, and members cannot stop or alter the shadow
|
|
10
|
+
* trail AWS materialises for them.
|
|
11
|
+
*/
|
|
12
|
+
export class OrganisationTrail extends Construct {
|
|
13
|
+
constructor(scope, id, props) {
|
|
14
|
+
super(scope, id);
|
|
15
|
+
const trail = new Trail(this, "OrganisationTrail", {
|
|
16
|
+
// "org" short form keeps the name inside S3's 63-character limit for
|
|
17
|
+
// long region names.
|
|
18
|
+
bucketName: `cloudtrail-org-management-events-${props.managementAccountId}-${props.region}`,
|
|
19
|
+
trailName: ORGANISATION_TRAIL_NAME,
|
|
20
|
+
isMultiRegionTrail: true,
|
|
21
|
+
isOrganizationTrail: true,
|
|
22
|
+
orgId: props.orgId,
|
|
23
|
+
retainAuditHistory: true
|
|
24
|
+
});
|
|
25
|
+
new CfnOutput(this, "OrganisationTrailBucketNameOutput", {
|
|
26
|
+
key: ORG_TRAIL_BUCKET_OUTPUT_KEY,
|
|
27
|
+
value: trail.bucket.bucketName,
|
|
28
|
+
exportName: ORG_TRAIL_BUCKET_OUTPUT_KEY,
|
|
29
|
+
description: "Organisation trail bucket, read by the org-trail migration reconciler"
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -29,7 +29,16 @@ function buildFoundationGuardrails(allowedRegions) {
|
|
|
29
29
|
{
|
|
30
30
|
Sid: "DenyRootUserActions",
|
|
31
31
|
Effect: "Deny",
|
|
32
|
-
|
|
32
|
+
// Deny + NotAction denies everything except the policy-management
|
|
33
|
+
// scopes a root-task unlock session needs; the AWS root-task policies
|
|
34
|
+
// constrain those sessions further.
|
|
35
|
+
NotAction: [
|
|
36
|
+
"s3:GetBucketPolicy",
|
|
37
|
+
"s3:PutBucketPolicy",
|
|
38
|
+
"s3:DeleteBucketPolicy",
|
|
39
|
+
"sqs:GetQueueAttributes",
|
|
40
|
+
"sqs:SetQueueAttributes"
|
|
41
|
+
],
|
|
33
42
|
Resource: "*",
|
|
34
43
|
Condition: {
|
|
35
44
|
StringLike: {
|
|
@@ -18,6 +18,7 @@ export interface AccountProps extends StackProps {
|
|
|
18
18
|
export declare class Account extends Stack {
|
|
19
19
|
readonly organisationType: OrganisationType;
|
|
20
20
|
protected readonly resolvedRegion: string;
|
|
21
|
+
protected readonly accountGlobalsConfigured: boolean;
|
|
21
22
|
constructor(scope: Construct, id: string, props: AccountProps);
|
|
22
23
|
/**
|
|
23
24
|
* Whether this account receives the OIDC deploy connector (provider +
|
|
@@ -34,6 +35,14 @@ export declare class Account extends Stack {
|
|
|
34
35
|
* Deliberately separate from the OIDC/IPAM gates — the axes are independent.
|
|
35
36
|
*/
|
|
36
37
|
protected createsEcrDefaultImage(): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Whether this account owns its own management-events trail. Standalone,
|
|
40
|
+
* Platform, and member accounts do (subject to the fjallAccountTrailState
|
|
41
|
+
* decommission lifecycle); the organisation root overrides to `false`
|
|
42
|
+
* because its OrganisationTrail records this account's events too.
|
|
43
|
+
* Deliberately separate from the OIDC/ECR gates — the axes are independent.
|
|
44
|
+
*/
|
|
45
|
+
protected createsManagementEventsTrail(): boolean;
|
|
37
46
|
enableGuardDuty(props?: GuardDutyDetectorProps): GuardDutyDetector;
|
|
38
47
|
enableSecurityHub(props?: SecurityHubHubProps): SecurityHubHub;
|
|
39
48
|
enableConfigRecorder(props?: ConfigRecorderProps): ConfigRecorder;
|
|
@@ -4,7 +4,7 @@ import { ManagementEventsTrail } from "../../config/aws/cloudTrail.js";
|
|
|
4
4
|
import { OidcConnector } from "../../config/aws/oidcConnector.js";
|
|
5
5
|
import { AccountMonitoringRole } from "../../config/aws/accountMonitoringRole.js";
|
|
6
6
|
import { AccountAuditRole } from "../../config/aws/accountAuditRole.js";
|
|
7
|
-
import { CDK_CONTEXT_KEYS } from "../../utils/cdkContext.js";
|
|
7
|
+
import { CDK_CONTEXT_KEYS, resolveAccountTrailState } from "../../utils/cdkContext.js";
|
|
8
8
|
import { getConfig } from "../../utils/getConfig.js";
|
|
9
9
|
import { DisasterRecovery } from "../../config/aws/disasterRecovery.js";
|
|
10
10
|
import { S3BlockPublicAccess } from "../../config/aws/s3BlockPublicAccess.js";
|
|
@@ -18,6 +18,7 @@ import { InspectorEnablement } from "../../config/aws/inspectorEnablement.js";
|
|
|
18
18
|
export class Account extends Stack {
|
|
19
19
|
organisationType = "account";
|
|
20
20
|
resolvedRegion;
|
|
21
|
+
accountGlobalsConfigured;
|
|
21
22
|
constructor(scope, id, props) {
|
|
22
23
|
const config = getConfig();
|
|
23
24
|
const accountId = props.accountId ?? config.accountId;
|
|
@@ -59,6 +60,7 @@ export class Account extends Stack {
|
|
|
59
60
|
// (OIDC provider/role, FjallMonitoring, FjallAudit) have fixed names that
|
|
60
61
|
// collide across regions, so they are created only in the home region.
|
|
61
62
|
const accountGlobalsConfigured = this.node.tryGetContext("fjallAccountGlobalsConfigured") === "true";
|
|
63
|
+
this.accountGlobalsConfigured = accountGlobalsConfigured;
|
|
62
64
|
if (this.receivesDeployRole() &&
|
|
63
65
|
fjallOrgId &&
|
|
64
66
|
!oidcAlreadyConfigured &&
|
|
@@ -71,17 +73,28 @@ export class Account extends Stack {
|
|
|
71
73
|
if (fjallOrgId && !accountGlobalsConfigured) {
|
|
72
74
|
new AccountAuditRole(this, "AuditRole", { fjallOrgId });
|
|
73
75
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
const environment = config.environment ?? "unknown";
|
|
77
|
+
// The trail is multi-region, so a non-home-region cascade stack creating
|
|
78
|
+
// its own copy records every management event a second time — CloudTrail
|
|
79
|
+
// bills additional copies at $2.00/100k events. Home region only, like
|
|
80
|
+
// the other account globals above.
|
|
81
|
+
const trailState = resolveAccountTrailState(this.node);
|
|
82
|
+
if (!accountGlobalsConfigured &&
|
|
83
|
+
this.createsManagementEventsTrail() &&
|
|
84
|
+
trailState !== "removed") {
|
|
85
|
+
new ManagementEventsTrail(this, "CloudTrail", {
|
|
86
|
+
accountId: this.account,
|
|
87
|
+
region: this.resolvedRegion,
|
|
88
|
+
lifecycleState: trailState,
|
|
89
|
+
retainAuditHistory: environment !== "development"
|
|
90
|
+
});
|
|
91
|
+
}
|
|
78
92
|
if (this.createsEcrDefaultImage()) {
|
|
79
93
|
new EcrDefaultImage(this, "EcrDefaultImage", {
|
|
80
94
|
region: this.resolvedRegion,
|
|
81
95
|
accountId: this.account
|
|
82
96
|
});
|
|
83
97
|
}
|
|
84
|
-
const environment = config.environment ?? "unknown";
|
|
85
98
|
if (config.disasterRecoveryRegion) {
|
|
86
99
|
const isComplianceAccount = environment === "compliance";
|
|
87
100
|
if (environment === "production" || isComplianceAccount) {
|
|
@@ -124,6 +137,16 @@ export class Account extends Stack {
|
|
|
124
137
|
createsEcrDefaultImage() {
|
|
125
138
|
return true;
|
|
126
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Whether this account owns its own management-events trail. Standalone,
|
|
142
|
+
* Platform, and member accounts do (subject to the fjallAccountTrailState
|
|
143
|
+
* decommission lifecycle); the organisation root overrides to `false`
|
|
144
|
+
* because its OrganisationTrail records this account's events too.
|
|
145
|
+
* Deliberately separate from the OIDC/ECR gates — the axes are independent.
|
|
146
|
+
*/
|
|
147
|
+
createsManagementEventsTrail() {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
127
150
|
enableGuardDuty(props) {
|
|
128
151
|
return new GuardDutyDetector(this, "GuardDuty", props);
|
|
129
152
|
}
|
|
@@ -47,6 +47,15 @@ export declare class Organisation extends Account {
|
|
|
47
47
|
* default-ECR-image helper.
|
|
48
48
|
*/
|
|
49
49
|
protected createsEcrDefaultImage(): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* The organisation root owns the org-wide OrganisationTrail, which records
|
|
52
|
+
* this account's management events too — an ACTIVE per-account trail here
|
|
53
|
+
* would be a duplicate paid copy. Returning `false` skips Account's active
|
|
54
|
+
* synthesis; the root's own constructor instead keeps the legacy construct
|
|
55
|
+
* in draining form until the lifecycle reaches "org", so pre-existing audit
|
|
56
|
+
* storage survives the upgrade.
|
|
57
|
+
*/
|
|
58
|
+
protected createsManagementEventsTrail(): boolean;
|
|
50
59
|
private createAccountReferences;
|
|
51
60
|
private setupOrganisationFeatures;
|
|
52
61
|
private setupCostAllocationTagActivator;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import App from "../../app.js";
|
|
2
2
|
import { Account } from "./account.js";
|
|
3
3
|
import { IdentityCenter } from "../../config/aws/identityCenter.js";
|
|
4
|
+
import { ManagementEventsTrail } from "../../config/aws/cloudTrail.js";
|
|
5
|
+
import { OrganisationTrail } from "../../config/aws/organisationTrail.js";
|
|
4
6
|
import { ScpPreset } from "../../config/aws/scpPreset.js";
|
|
5
7
|
import { OrganisationResource, OrganisationAccount, CostAllocationTagActivator } from "../../resources/aws/organisation/index.js";
|
|
6
8
|
import { stripAndCamelCase } from "../../utils/stripAndCamelCase.js";
|
|
7
|
-
import { CDK_CONTEXT_KEYS } from "../../utils/cdkContext.js";
|
|
9
|
+
import { CDK_CONTEXT_KEYS, resolveAccountTrailState } from "../../utils/cdkContext.js";
|
|
8
10
|
import { getConfig } from "../../utils/getConfig.js";
|
|
9
11
|
import { extractAccountNames } from "../../utils/accountsUtils.js";
|
|
10
12
|
/**
|
|
@@ -39,10 +41,17 @@ export class Organisation extends Account {
|
|
|
39
41
|
this.accountMap[key.toLowerCase()] = value;
|
|
40
42
|
}
|
|
41
43
|
this.accountsConfig = props.accounts;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
// CDK context is a `??` boundary: `-c key=` legitimately passes "", which
|
|
45
|
+
// would flow through `??`, defeat the `this.account` fallback, and break
|
|
46
|
+
// the SSO management-account exclusion downstream.
|
|
47
|
+
const orgIdCtx = this.node.tryGetContext(CDK_CONTEXT_KEYS.ORG_ID);
|
|
48
|
+
const rootIdCtx = this.node.tryGetContext(CDK_CONTEXT_KEYS.ROOT_ID);
|
|
49
|
+
const managementAccountCtx = this.node.tryGetContext(CDK_CONTEXT_KEYS.MANAGEMENT_ACCOUNT_ID);
|
|
50
|
+
const orgId = typeof orgIdCtx === "string" && orgIdCtx !== "" ? orgIdCtx : undefined;
|
|
51
|
+
const rootId = typeof rootIdCtx === "string" && rootIdCtx !== "" ? rootIdCtx : undefined;
|
|
52
|
+
const managementAccountId = (typeof managementAccountCtx === "string" && managementAccountCtx !== ""
|
|
53
|
+
? managementAccountCtx
|
|
54
|
+
: undefined) ?? this.account;
|
|
46
55
|
if (!orgId || !rootId) {
|
|
47
56
|
throw new Error("orgId and rootId must be provided via CDK context. Run deployment through the Fjall CLI.");
|
|
48
57
|
}
|
|
@@ -51,6 +60,32 @@ export class Organisation extends Account {
|
|
|
51
60
|
rootId,
|
|
52
61
|
managementAccountId
|
|
53
62
|
});
|
|
63
|
+
// Home region only: the trail is multi-region, so an org-region cascade
|
|
64
|
+
// stack creating a second org-wide trail would duplicate every paid copy
|
|
65
|
+
// across the whole organisation.
|
|
66
|
+
if (!this.accountGlobalsConfigured) {
|
|
67
|
+
new OrganisationTrail(this, "OrganisationCloudTrail", {
|
|
68
|
+
orgId,
|
|
69
|
+
managementAccountId,
|
|
70
|
+
region: this.resolvedRegion
|
|
71
|
+
});
|
|
72
|
+
// Already-deployed roots carry a per-account trail whose bucket is
|
|
73
|
+
// DESTROY + autoDeleteObjects — dropping the construct outright would
|
|
74
|
+
// delete that audit history, bypassing the acknowledgeTrailHistoryLoss
|
|
75
|
+
// gate. Keep it in DRAINING form (same "CloudTrail" construct path as
|
|
76
|
+
// Account ⇒ identical bucket/CMK logical IDs): the trail resource goes,
|
|
77
|
+
// bucket + CMK flip to RETAIN until the reconciler decommissions them
|
|
78
|
+
// behind the gate. "removed" (trailLifecycle "org") synthesises nothing.
|
|
79
|
+
const trailState = resolveAccountTrailState(this.node);
|
|
80
|
+
if (trailState !== "removed") {
|
|
81
|
+
new ManagementEventsTrail(this, "CloudTrail", {
|
|
82
|
+
accountId: this.account,
|
|
83
|
+
region: this.resolvedRegion,
|
|
84
|
+
lifecycleState: "draining",
|
|
85
|
+
retainAuditHistory: true
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
54
89
|
this.createAccountReferences(props);
|
|
55
90
|
this.setupOrganisationFeatures(props.identityCenter ?? true, managementAccountId);
|
|
56
91
|
}
|
|
@@ -70,6 +105,17 @@ export class Organisation extends Account {
|
|
|
70
105
|
createsEcrDefaultImage() {
|
|
71
106
|
return false;
|
|
72
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* The organisation root owns the org-wide OrganisationTrail, which records
|
|
110
|
+
* this account's management events too — an ACTIVE per-account trail here
|
|
111
|
+
* would be a duplicate paid copy. Returning `false` skips Account's active
|
|
112
|
+
* synthesis; the root's own constructor instead keeps the legacy construct
|
|
113
|
+
* in draining form until the lifecycle reaches "org", so pre-existing audit
|
|
114
|
+
* storage survives the upgrade.
|
|
115
|
+
*/
|
|
116
|
+
createsManagementEventsTrail() {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
73
119
|
createAccountReferences(props) {
|
|
74
120
|
const allNames = extractAccountNames(props.accounts);
|
|
75
121
|
for (const accountName of allNames) {
|
|
@@ -38,7 +38,7 @@ export interface S3Props {
|
|
|
38
38
|
readonly backupVaultTier?: BackupTier;
|
|
39
39
|
readonly cors?: CorsRule[];
|
|
40
40
|
readonly deployment?: S3DeploymentConfig;
|
|
41
|
-
/** When true, sets RemovalPolicy.RETAIN
|
|
41
|
+
/** When true, sets RemovalPolicy.RETAIN (overriding the env-aware default). Used for imported buckets. */
|
|
42
42
|
readonly retain?: boolean;
|
|
43
43
|
}
|
|
44
44
|
export interface StorageBuildProps extends S3Props {
|
|
@@ -21,7 +21,11 @@ function toHttpMethod(method) {
|
|
|
21
21
|
DELETE: HttpMethods.DELETE,
|
|
22
22
|
HEAD: HttpMethods.HEAD
|
|
23
23
|
};
|
|
24
|
-
|
|
24
|
+
const mapped = methodMap[method];
|
|
25
|
+
if (mapped === undefined) {
|
|
26
|
+
throw new Error(`Unsupported CORS method "${method}" — expected one of: ${Object.keys(methodMap).join(", ")}.`);
|
|
27
|
+
}
|
|
28
|
+
return mapped;
|
|
25
29
|
}
|
|
26
30
|
function toCorsRules(cors) {
|
|
27
31
|
if (!cors)
|
|
@@ -1,14 +1,61 @@
|
|
|
1
1
|
import { Stack } from "aws-cdk-lib";
|
|
2
2
|
import * as CloudTrail from "aws-cdk-lib/aws-cloudtrail";
|
|
3
3
|
import { Construct } from "constructs";
|
|
4
|
+
import { CustomerManagedKey } from "../secrets/index.js";
|
|
4
5
|
import { S3Bucket } from "../storage/index.js";
|
|
5
6
|
interface CloudTrailProps extends CloudTrail.TrailProps {
|
|
6
7
|
bucketName: string;
|
|
8
|
+
/**
|
|
9
|
+
* Bucket + CMK survive stack deletion (RemovalPolicy.RETAIN). The trail
|
|
10
|
+
* resource itself stays DESTROY regardless — a retained trail keeps
|
|
11
|
+
* logging (and charging) as an unmanaged orphan. Default false preserves
|
|
12
|
+
* the historical TrailStack behaviour.
|
|
13
|
+
*/
|
|
14
|
+
retainAuditHistory?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Draining mode: synthesise only the bucket + CMK so CloudFormation deletes
|
|
17
|
+
* the trail resource while the retained storage keeps its logical IDs (and
|
|
18
|
+
* so its history) intact.
|
|
19
|
+
*/
|
|
20
|
+
omitTrail?: boolean;
|
|
7
21
|
}
|
|
22
|
+
export declare function validateTrailProps(props: CloudTrailProps): void;
|
|
8
23
|
export declare class Trail extends Construct {
|
|
9
|
-
readonly trail
|
|
24
|
+
readonly trail?: CloudTrail.Trail;
|
|
10
25
|
readonly bucket: S3Bucket;
|
|
26
|
+
readonly encryptionKey: CustomerManagedKey;
|
|
11
27
|
constructor(scope: Construct, id: string, props: CloudTrailProps);
|
|
28
|
+
private trailArn;
|
|
29
|
+
/**
|
|
30
|
+
* Canonical CloudTrail delivery policy (ACL probe + log write), scoped to
|
|
31
|
+
* this trail's ARN via aws:SourceArn so no other trail — including one in
|
|
32
|
+
* another account — can deliver into or probe the bucket. Emitted from
|
|
33
|
+
* this construct rather than left to the L2 Trail because draining mode
|
|
34
|
+
* (omitTrail) skips the L2 construct while the trail being deleted may
|
|
35
|
+
* still deliver mid-update. Fjall-prefixed sids avoid colliding with the
|
|
36
|
+
* sid-less statements the L2 Trail adds when it is constructed.
|
|
37
|
+
*/
|
|
38
|
+
private addAccountTrailBucketPolicy;
|
|
39
|
+
/**
|
|
40
|
+
* Conditioned replacement for the KMS half of the retired grantReadWrite:
|
|
41
|
+
* `Bucket.grantReadWrite` silently granted the CloudTrail principal
|
|
42
|
+
* unconditioned Encrypt/Decrypt/ReEncrypt on the CMK because the bucket
|
|
43
|
+
* carries an encryptionKey. CloudTrail's actual needs are
|
|
44
|
+
* kms:GenerateDataKey* + kms:DescribeKey (trail-side log encryption,
|
|
45
|
+
* validated at CreateTrail) plus digest delivery via the bucket's default
|
|
46
|
+
* SSE-KMS — without these, trail creation fails with
|
|
47
|
+
* InsufficientEncryptionPolicyException.
|
|
48
|
+
*/
|
|
49
|
+
private addAccountTrailKeyPolicy;
|
|
50
|
+
/**
|
|
51
|
+
* The CDK L2 Trail contributes no KMS key-policy statements at all — it
|
|
52
|
+
* only sets kmsKeyId on the CfnTrail (verified in aws-cdk-lib 2.251.0).
|
|
53
|
+
* These three statements are therefore the entire CloudTrail-facing key
|
|
54
|
+
* policy: management-trail and member shadow-trail encryption plus S3
|
|
55
|
+
* digest delivery. None can be narrowed to member-only encryption contexts
|
|
56
|
+
* without breaking the management trail's own delivery.
|
|
57
|
+
*/
|
|
58
|
+
private addOrganisationTrailKeyPolicy;
|
|
12
59
|
}
|
|
13
60
|
export declare class TrailStack extends Stack {
|
|
14
61
|
constructor(scope: Construct, id: string, props: CloudTrailProps);
|