mirror of
https://github.com/Sea-Haven-Industries/seahaven-org-baseline.git
synced 2026-10-04 00:43:13 +00:00
feat(iam): add seahaven-hcptf and managed site policies (PLAT-79) (#174)
Add seahaven-hcptf in prod and dev for the imported payments-dashboard HCP roles and Lambda boundary, and move seahaven-site-hcptf inline policies to managed policies.
This commit is contained in:
parent
86666b8d0b
commit
c6de4e0c7f
8 changed files with 1448 additions and 43 deletions
8
.github/workflows/deploy.yaml
vendored
8
.github/workflows/deploy.yaml
vendored
|
|
@ -47,7 +47,9 @@ jobs:
|
|||
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@e5691d8a7f96ac4d5a841a82975ff0a4354d53ac # v1.0.7
|
||||
with:
|
||||
node-version: "24"
|
||||
stacks: "dev-baseline deploy-substrate-dev"
|
||||
# seahaven-hcptf-dev was imported. The deploy creates the three managed
|
||||
# policies and removes the inline policies. Do not create the roles.
|
||||
stacks: "dev-baseline deploy-substrate-dev seahaven-hcptf-dev"
|
||||
stack-name: "seahaven-dev-baseline"
|
||||
secrets:
|
||||
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN_DEV }}
|
||||
|
|
@ -57,7 +59,9 @@ jobs:
|
|||
with:
|
||||
node-version: "24"
|
||||
# seahaven-site-hcptf was imported after the roles left terraform-substrate.
|
||||
stacks: "prod-baseline dynamodb-cmk-prod alarm-topic-prod deploy-substrate-prod app-web-acl-prod seahaven-site-hcptf"
|
||||
# seahaven-hcptf was imported. The deploy creates the three managed
|
||||
# policies and removes the inline policies. Do not create the roles.
|
||||
stacks: "prod-baseline dynamodb-cmk-prod alarm-topic-prod deploy-substrate-prod app-web-acl-prod seahaven-site-hcptf seahaven-hcptf"
|
||||
stack-name: "seahaven-prod-baseline"
|
||||
secrets:
|
||||
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN_PROD }}
|
||||
|
|
|
|||
13
README.md
13
README.md
|
|
@ -33,7 +33,8 @@ are noted):
|
|||
| `seahaven-engineering-access` | 328440206208 | us-east-1 | Identity Center group `engineering`. `EngineeringProd` reads payments-dashboard configuration and seahaven-site in 011934824531. `EngineeringDev` has no allow (PLAT-235, PLAT-236). |
|
||||
| `seahaven-external-dev-baseline` | 396287094661 | us-east-1 | Member-account baseline: Config, GuardDuty, Security Hub (FSBP + CIS v3.0), Access Analyzer, flow logs, budget |
|
||||
| `seahaven-terraform-substrate` | 011934824531, 710827005802 | us-east-1 | Removed from the CDK app and from CD (PLAT-147). Live stacks remain until `scripts/delete-terraform-substrate-prod-dev.sh`. The six imported prod pairs are already forgotten. |
|
||||
| `seahaven-site-hcptf` | 011934824531 | us-east-1 | Imported `hcptf-seahaven-site` apply and plan roles (PLAT-225). On the prod deploy job. Do not create. |
|
||||
| `seahaven-site-hcptf` | 011934824531 | us-east-1 | Imported `hcptf-seahaven-site` apply and plan roles (PLAT-225). Policies are managed at `/tf-managed/`. On the prod deploy job. Do not create the roles. |
|
||||
| `seahaven-hcptf` | 011934824531, 710827005802 | us-east-1 | payments-dashboard HCP apply and plan roles, scoped policies, and the Lambda boundary. Trust is pinned to `payments-dashboard-prod` in project `seahaven-prod`, and to `payments-dashboard-dev` in project `seahaven-dev`. Roles and boundary are imported. On the dev and prod deploy jobs. |
|
||||
| `seahaven-terraform-substrate` | 396287094661 | us-east-1 | Staged manually for SHOC backend/frontend adoption; exact HCP roles and deploy boundaries referencing the existing OIDC provider. Stays (PLAT-148). |
|
||||
| `seahaven-security-baseline` | 001520130573 | us-east-1 | Member-account baseline for the delegated security-admin account (same construct set) |
|
||||
| `seahaven-dev-baseline` | 710827005802 | us-east-1 | Member-account baseline for internal dev/staging (org-managed detection — no local GuardDuty/SecurityHub) |
|
||||
|
|
@ -77,6 +78,9 @@ the TypeScript source — no separate compile step needed for `cdk synth` /
|
|||
| `dynamodb-cmk-prod` | `seahaven-dynamodb-cmk` | 011934824531 | us-east-1 | `lib/dynamodb-cmk-stack.ts` |
|
||||
| `alarm-topic-prod` | `seahaven-alarm-topic` | 011934824531 | us-east-1 | `lib/alarm-topic-stack.ts` |
|
||||
| `app-web-acl-prod` | `seahaven-app-web-acl` | 011934824531 | us-east-1 | `lib/app-web-acl-stack.ts` |
|
||||
| `seahaven-site-hcptf` | `seahaven-site-hcptf` | 011934824531 | us-east-1 | `lib/seahaven-site-hcptf-stack.ts` |
|
||||
| `seahaven-hcptf` | `seahaven-hcptf` | 011934824531 | us-east-1 | `lib/seahaven-hcptf-stack.ts` |
|
||||
| `seahaven-hcptf-dev` | `seahaven-hcptf` | 710827005802 | us-east-1 | `lib/seahaven-hcptf-stack.ts` |
|
||||
|
||||
Member-account stacks deploy with per-account credentials — the CD workflow
|
||||
runs one job per account, each assuming that account's OIDC deploy role. Local
|
||||
|
|
@ -236,7 +240,12 @@ Console / one-shot CLI owns only:
|
|||
Do not manage prod/dev workload IAM (`hcptf-<stack>` pairs, Lambda exec
|
||||
roles, `policy/tf-managed/<stack>` ceilings) in the console. Do not append
|
||||
new prod/dev `hcptf-<stack>` pairs to this template. New prod/dev HCP stacks
|
||||
do not need an org-baseline IAM PR.
|
||||
do not need an org-baseline IAM PR. payments-dashboard HCP roles and
|
||||
the Lambda boundary are stack `seahaven-hcptf` in prod and dev. Both copies
|
||||
are imported and are on the deploy jobs. The deploy creates
|
||||
`payments-dashboard-hcptf-iam`, `payments-dashboard-hcptf-services`, and
|
||||
`payments-dashboard-hcptf-plan`, and removes the inline policies. Do not
|
||||
create the roles or the boundary.
|
||||
|
||||
**External-dev IAM stays in this repo (PLAT-148).** SHOC backend/frontend HCP
|
||||
roles, SHOC deploy/runtime boundaries, `shoc-frontend-resources.ts`, and
|
||||
|
|
|
|||
52
bin/app.ts
52
bin/app.ts
|
|
@ -10,6 +10,7 @@ import { DeploySubstrateStack } from "../lib/deploy-substrate-stack";
|
|||
import { TerraformSubstrateStack } from "../lib/terraform-substrate-stack";
|
||||
import { DynamoDbCmkStack } from "../lib/dynamodb-cmk-stack";
|
||||
import { AppWebAclStack } from "../lib/app-web-acl-stack";
|
||||
import { SeahavenHcptfStack } from "../lib/seahaven-hcptf-stack";
|
||||
import { SeahavenSiteHcptfStack } from "../lib/seahaven-site-hcptf-stack";
|
||||
import { MemberBaselineStack } from "../lib/member-baseline-stack";
|
||||
import { OrgGovernanceStack } from "../lib/org-governance-stack";
|
||||
|
|
@ -232,11 +233,62 @@ new AppWebAclStack(app, "app-web-acl-prod", {
|
|||
|
||||
// seahaven-site exec roles (PLAT-225). Not part of terraform-substrate.
|
||||
// Imported. On the prod deploy job. A create fails because the roles already exist.
|
||||
// Inline policies are managed policies at /tf-managed/. Do not recreate the roles.
|
||||
new SeahavenSiteHcptfStack(app, "seahaven-site-hcptf", {
|
||||
stackName: "seahaven-site-hcptf",
|
||||
env: { account: PROD_ACCOUNT, region: "us-east-1" },
|
||||
});
|
||||
|
||||
// payments-dashboard HCP roles, scoped policies, and the Lambda boundary.
|
||||
// The same three names already exist in prod and dev. Import them with
|
||||
// `-c hcptfPaymentsImport=true`. A create fails. Neither stack is on a
|
||||
// deploy job until that import succeeds. Trust is pinned per workspace.
|
||||
const hcptfPaymentsImport = contextBoolean("hcptfPaymentsImport");
|
||||
const paymentsSecrets = (
|
||||
account: string,
|
||||
suffixes: readonly string[],
|
||||
): string[] =>
|
||||
suffixes.map(
|
||||
(suffix) =>
|
||||
`arn:aws:secretsmanager:us-east-1:${account}:secret:payments-dashboard/${suffix}`,
|
||||
);
|
||||
|
||||
new SeahavenHcptfStack(app, "seahaven-hcptf", {
|
||||
stackName: "seahaven-hcptf",
|
||||
env: { account: PROD_ACCOUNT, region: "us-east-1" },
|
||||
hcpProject: "seahaven-prod",
|
||||
hcpWorkspace: "payments-dashboard-prod",
|
||||
dynamodbCmkArn:
|
||||
"arn:aws:kms:us-east-1:011934824531:key/be5fa4cb-c546-40fe-a13d-c7bec79f5d12",
|
||||
secretArns: paymentsSecrets(PROD_ACCOUNT, [
|
||||
"slack-bot-token-0pAM3S",
|
||||
"slack-signing-secret-u0T6h8",
|
||||
"boa-check-mgmt-LEbC65",
|
||||
"boa-reporting-JoR9lq",
|
||||
"expense-slack-token-SeMg3s",
|
||||
"expense-slack-signing-secret-lbb78J",
|
||||
]),
|
||||
importExisting: hcptfPaymentsImport,
|
||||
});
|
||||
|
||||
new SeahavenHcptfStack(app, "seahaven-hcptf-dev", {
|
||||
stackName: "seahaven-hcptf",
|
||||
env: { account: DEV_ACCOUNT, region: "us-east-1" },
|
||||
hcpProject: "seahaven-dev",
|
||||
hcpWorkspace: "payments-dashboard-dev",
|
||||
dynamodbCmkArn:
|
||||
"arn:aws:kms:us-east-1:710827005802:key/600997e6-418e-4b7f-9d63-cd42a7505a95",
|
||||
secretArns: paymentsSecrets(DEV_ACCOUNT, [
|
||||
"slack-bot-token-KhPaLp",
|
||||
"slack-signing-secret-CDxsUK",
|
||||
"boa-check-mgmt-kkEnCw",
|
||||
"boa-reporting-uMHHVo",
|
||||
"expense-slack-token-05OZg3",
|
||||
"expense-slack-signing-secret-DQAoXS",
|
||||
]),
|
||||
importExisting: hcptfPaymentsImport,
|
||||
});
|
||||
|
||||
// External-dev already has app.terraform.io federation. Both role gates start
|
||||
// false in cdk.json: POC is enabled by a normal update; dev/staging only by
|
||||
// CloudFormation import after Terraform relinquishes those four live roles.
|
||||
|
|
|
|||
11
import-maps/seahaven-hcptf-dev.json
Normal file
11
import-maps/seahaven-hcptf-dev.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"PaymentsDashboardApplyRole": {
|
||||
"RoleName": "hcptf-payments-dashboard"
|
||||
},
|
||||
"PaymentsDashboardPlanRole": {
|
||||
"RoleName": "hcptf-payments-dashboard-plan"
|
||||
},
|
||||
"PaymentsDashboardLambdaBoundary": {
|
||||
"PolicyArn": "arn:aws:iam::710827005802:policy/tf-managed/payments-dashboard-lambda-boundary"
|
||||
}
|
||||
}
|
||||
11
import-maps/seahaven-hcptf-prod.json
Normal file
11
import-maps/seahaven-hcptf-prod.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"PaymentsDashboardApplyRole": {
|
||||
"RoleName": "hcptf-payments-dashboard"
|
||||
},
|
||||
"PaymentsDashboardPlanRole": {
|
||||
"RoleName": "hcptf-payments-dashboard-plan"
|
||||
},
|
||||
"PaymentsDashboardLambdaBoundary": {
|
||||
"PolicyArn": "arn:aws:iam::011934824531:policy/tf-managed/payments-dashboard-lambda-boundary"
|
||||
}
|
||||
}
|
||||
58
lib/hcptf-policy-aspect.ts
Normal file
58
lib/hcptf-policy-aspect.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import * as cdk from "aws-cdk-lib";
|
||||
import * as iam from "aws-cdk-lib/aws-iam";
|
||||
import { IConstruct } from "constructs";
|
||||
|
||||
/** IAM managed-policy quota, whitespace excluded. */
|
||||
const MAX_POLICY_CHARS = 6144;
|
||||
|
||||
/**
|
||||
* Fails synth when an HCP stack's managed policy document reaches the IAM
|
||||
* size quota, or when a role in that stack carries an inline policy.
|
||||
* Register it on seahaven-hcptf and seahaven-site-hcptf only.
|
||||
*
|
||||
* `allowInlinePolicies` is only for the one-time `cdk import` template.
|
||||
* That template has to name the live inline policies so the following
|
||||
* deploy can delete them. The default template still rejects inline policies.
|
||||
*/
|
||||
export class HcptfPolicyAspect implements cdk.IAspect {
|
||||
constructor(private readonly allowInlinePolicies = false) {}
|
||||
|
||||
public visit(node: IConstruct): void {
|
||||
if (node instanceof iam.CfnManagedPolicy) {
|
||||
const size = JSON.stringify(node.policyDocument).replace(/\s/g, "").length;
|
||||
if (size >= MAX_POLICY_CHARS) {
|
||||
cdk.Annotations.of(node).addError(
|
||||
`managed policy document is ${size} characters, whitespace excluded (limit ${MAX_POLICY_CHARS})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (node instanceof iam.CfnRole) {
|
||||
const policies = node.policies;
|
||||
if (Array.isArray(policies)) {
|
||||
for (const policy of policies) {
|
||||
if (cdk.Token.isUnresolved(policy) || !("policyDocument" in policy)) {
|
||||
continue;
|
||||
}
|
||||
const size = JSON.stringify(policy.policyDocument)
|
||||
.replace(/\s/g, "")
|
||||
.length;
|
||||
if (size >= MAX_POLICY_CHARS) {
|
||||
cdk.Annotations.of(node).addError(
|
||||
`inline policy document is ${size} characters, whitespace excluded (limit ${MAX_POLICY_CHARS})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!this.allowInlinePolicies && policies.length > 0) {
|
||||
cdk.Annotations.of(node).addError(
|
||||
"inline policy is not allowed on this role",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node instanceof iam.CfnPolicy) {
|
||||
cdk.Annotations.of(node).addError("inline policy is not allowed in this stack");
|
||||
}
|
||||
}
|
||||
}
|
||||
1232
lib/seahaven-hcptf-stack.ts
Normal file
1232
lib/seahaven-hcptf-stack.ts
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,7 @@
|
|||
import * as cdk from "aws-cdk-lib";
|
||||
import * as iam from "aws-cdk-lib/aws-iam";
|
||||
import { Construct } from "constructs";
|
||||
import { HcptfPolicyAspect } from "./hcptf-policy-aspect";
|
||||
|
||||
/**
|
||||
* Prod exec roles for the seahaven-site HCP workspace (PLAT-225).
|
||||
|
|
@ -10,7 +11,9 @@ import { Construct } from "constructs";
|
|||
* The stack is on the prod deploy job.
|
||||
*
|
||||
* Import identifiers were the role names `hcptf-seahaven-site` and
|
||||
* `hcptf-seahaven-site-plan`.
|
||||
* `hcptf-seahaven-site-plan`. Construct ids stay ApplyRole and PlanRole.
|
||||
* Inline policies are managed policies at /tf-managed/. Do not rename
|
||||
* the roles.
|
||||
*/
|
||||
export class SeahavenSiteHcptfStack extends cdk.Stack {
|
||||
constructor(scope: Construct, id: string, props: cdk.StackProps) {
|
||||
|
|
@ -26,21 +29,39 @@ export class SeahavenSiteHcptfStack extends cdk.Stack {
|
|||
const githubOidc = `arn:aws:iam::${account}:oidc-provider/token.actions.githubusercontent.com`;
|
||||
const hcpOidc = `arn:aws:iam::${account}:oidc-provider/app.terraform.io`;
|
||||
|
||||
const iamPolicy = managedPolicy(
|
||||
this,
|
||||
"IamPolicy",
|
||||
"seahaven-site-hcptf-iam",
|
||||
scopedIamPolicy(account, deployRole, boundary),
|
||||
);
|
||||
const services = managedPolicy(
|
||||
this,
|
||||
"ServicesPolicy",
|
||||
"seahaven-site-hcptf-services",
|
||||
servicesPolicy(bucket, deployParams, wafParam, githubOidc),
|
||||
);
|
||||
const planRefresh = managedPolicy(
|
||||
this,
|
||||
"PlanPolicy",
|
||||
"seahaven-site-hcptf-plan",
|
||||
planPolicy(
|
||||
account,
|
||||
bucket,
|
||||
functionArn,
|
||||
deployParams,
|
||||
wafParam,
|
||||
githubOidc,
|
||||
deployRole,
|
||||
boundary,
|
||||
),
|
||||
);
|
||||
|
||||
const apply = new iam.CfnRole(this, "ApplyRole", {
|
||||
roleName: "hcptf-seahaven-site",
|
||||
maxSessionDuration: 3600,
|
||||
assumeRolePolicyDocument: trust(hcpOidc, "apply"),
|
||||
managedPolicyArns: [],
|
||||
policies: [
|
||||
{
|
||||
policyName: "seahaven-site-services",
|
||||
policyDocument: servicesPolicy(bucket, deployParams, wafParam, githubOidc),
|
||||
},
|
||||
{
|
||||
policyName: "scoped-iam-management",
|
||||
policyDocument: scopedIamPolicy(account, deployRole, boundary),
|
||||
},
|
||||
],
|
||||
managedPolicyArns: [iamPolicy.ref, services.ref],
|
||||
tags: roleTags(),
|
||||
});
|
||||
retain(apply);
|
||||
|
|
@ -49,35 +70,42 @@ export class SeahavenSiteHcptfStack extends cdk.Stack {
|
|||
roleName: "hcptf-seahaven-site-plan",
|
||||
maxSessionDuration: 3600,
|
||||
assumeRolePolicyDocument: trust(hcpOidc, "plan"),
|
||||
managedPolicyArns: ["arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"],
|
||||
policies: [
|
||||
{
|
||||
policyName: "seahaven-site-plan-refresh",
|
||||
policyDocument: planPolicy(
|
||||
account,
|
||||
bucket,
|
||||
functionArn,
|
||||
deployParams,
|
||||
wafParam,
|
||||
githubOidc,
|
||||
deployRole,
|
||||
boundary,
|
||||
),
|
||||
},
|
||||
managedPolicyArns: [
|
||||
"arn:aws:iam::aws:policy/job-function/ViewOnlyAccess",
|
||||
planRefresh.ref,
|
||||
],
|
||||
tags: roleTags(),
|
||||
});
|
||||
retain(plan);
|
||||
|
||||
new cdk.CfnOutput(this, "ApplyRoleArn", { value: apply.attrArn });
|
||||
new cdk.CfnOutput(this, "PlanRoleArn", { value: plan.attrArn });
|
||||
|
||||
cdk.Tags.of(this).add("Project", "seahaven-site");
|
||||
cdk.Tags.of(this).add("Owner", "adam@seahavenind.com");
|
||||
cdk.Tags.of(this).add("ManagedBy", "cdk");
|
||||
cdk.Aspects.of(this).add(new HcptfPolicyAspect());
|
||||
}
|
||||
}
|
||||
|
||||
function retain(role: iam.CfnRole): void {
|
||||
role.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.RETAIN;
|
||||
role.cfnOptions.updateReplacePolicy = cdk.CfnDeletionPolicy.RETAIN;
|
||||
function managedPolicy(
|
||||
scope: Construct,
|
||||
id: string,
|
||||
name: string,
|
||||
policyDocument: object,
|
||||
): iam.CfnManagedPolicy {
|
||||
const policy = new iam.CfnManagedPolicy(scope, id, {
|
||||
managedPolicyName: name,
|
||||
path: "/tf-managed/",
|
||||
policyDocument,
|
||||
});
|
||||
retain(policy);
|
||||
return policy;
|
||||
}
|
||||
|
||||
function retain(resource: cdk.CfnResource): void {
|
||||
resource.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.RETAIN;
|
||||
resource.cfnOptions.updateReplacePolicy = cdk.CfnDeletionPolicy.RETAIN;
|
||||
}
|
||||
|
||||
function roleTags(): cdk.CfnTag[] {
|
||||
|
|
@ -114,8 +142,8 @@ function servicesPolicy(
|
|||
deployParams: string,
|
||||
wafParam: string,
|
||||
githubOidc: string,
|
||||
): iam.PolicyDocument {
|
||||
return iam.PolicyDocument.fromJson({
|
||||
): object {
|
||||
return {
|
||||
Version: "2012-10-17",
|
||||
Statement: [
|
||||
{
|
||||
|
|
@ -201,11 +229,11 @@ function servicesPolicy(
|
|||
Resource: "*",
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function scopedIamPolicy(account: string, deployRole: string, boundary: string): iam.PolicyDocument {
|
||||
return iam.PolicyDocument.fromJson({
|
||||
function scopedIamPolicy(account: string, deployRole: string, boundary: string): object {
|
||||
return {
|
||||
Version: "2012-10-17",
|
||||
Statement: [
|
||||
{
|
||||
|
|
@ -343,7 +371,7 @@ function scopedIamPolicy(account: string, deployRole: string, boundary: string):
|
|||
Resource: `arn:aws:iam::${account}:policy/seahaven-*`,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function planPolicy(
|
||||
|
|
@ -355,8 +383,8 @@ function planPolicy(
|
|||
githubOidc: string,
|
||||
deployRole: string,
|
||||
boundary: string,
|
||||
): iam.PolicyDocument {
|
||||
return iam.PolicyDocument.fromJson({
|
||||
): object {
|
||||
return {
|
||||
Version: "2012-10-17",
|
||||
Statement: [
|
||||
{
|
||||
|
|
@ -467,5 +495,5 @@ function planPolicy(
|
|||
Resource: "*",
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue