diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index d7a9598..c0ad364 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -22,7 +22,7 @@ jobs: uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@e5691d8a7f96ac4d5a841a82975ff0a4354d53ac # v1.0.7 with: node-version: "24" - stacks: "account-baseline dynamodb-cmk regional-baseline-us-west-2 regional-baseline-us-east-2 backup-offsite backup org-governance platform-access engineering-access" + stacks: "account-baseline dynamodb-cmk regional-baseline-us-west-2 regional-baseline-us-east-2 backup-offsite backup org-governance platform-access engineering-access view-access" secrets: deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }} diff --git a/README.md b/README.md index 2d6a985..717f4a3 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ are noted): | `seahaven-backup-offsite` | 328440206208 | us-west-2 | Governance-locked offsite copy vault (C-7) | | `seahaven-org-governance` | 328440206208 | us-east-1 | AWS Organizations OU tree + SCPs (incl. the cdk-imported external-dev guardrails) | | `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-view-access` | 328440206208 | us-east-1 | Identity Center group `view`. Permission set `View` is `ViewOnlyAccess` on all five accounts. No data-plane reads and no assume-role. Membership is outside this stack. | | `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-hcptf` | 011934824531, 710827005802 | us-east-1 | payments-dashboard HCP apply and plan roles, scoped policies, and the Lambda boundary in prod and dev. Prod also owns the imported `hcptf-seahaven-site` apply and plan roles (PLAT-225), with policies at `/tf-managed/`. Trust is pinned per workspace. On the dev and prod deploy jobs. Do not create the roles. | @@ -67,6 +68,7 @@ the TypeScript source — no separate compile step needed for `cdk synth` / | `backup` | `seahaven-backup` | 328440206208 | us-east-1 | `lib/backup-stack.ts` | | `org-governance` | `seahaven-org-governance` | 328440206208 | us-east-1 | `lib/org-governance-stack.ts` | | `engineering-access` | `seahaven-engineering-access` | 328440206208 | us-east-1 | `lib/engineering-access-stack.ts` | +| `view-access` | `seahaven-view-access` | 328440206208 | us-east-1 | `lib/view-access-stack.ts` | | `external-dev-baseline` | `seahaven-external-dev-baseline` | 396287094661 | us-east-1 | `lib/member-baseline-stack.ts` | | `security-baseline` | `seahaven-security-baseline` | 001520130573 | us-east-1 | `lib/member-baseline-stack.ts` | | `dev-baseline` | `seahaven-dev-baseline` | 710827005802 | us-east-1 | `lib/member-baseline-stack.ts` (orgManagedDetection) | diff --git a/bin/app.ts b/bin/app.ts index 9ea80e5..361f7d9 100644 --- a/bin/app.ts +++ b/bin/app.ts @@ -15,6 +15,7 @@ import { MemberBaselineStack } from "../lib/member-baseline-stack"; import { OrgGovernanceStack } from "../lib/org-governance-stack"; import { PlatformAccessStack } from "../lib/platform-access-stack"; import { EngineeringAccessStack } from "../lib/engineering-access-stack"; +import { ViewAccessStack } from "../lib/view-access-stack"; const ACCOUNT = "328440206208"; const EXTERNAL_DEV_ACCOUNT = "396287094661"; @@ -95,6 +96,16 @@ new EngineeringAccessStack(app, "engineering-access", { prodAccountId: PROD_ACCOUNT, }); +new ViewAccessStack(app, "view-access", { + stackName: "seahaven-view-access", + env: { account: ACCOUNT, region: "us-east-1" }, // pragma: allowlist secret + managementAccountId: ACCOUNT, + securityAccountId: SECURITY_ACCOUNT, + externalDevAccountId: EXTERNAL_DEV_ACCOUNT, + devAccountId: DEV_ACCOUNT, + prodAccountId: PROD_ACCOUNT, +}); + new MemberBaselineStack(app, "external-dev-baseline", { stackName: "seahaven-external-dev-baseline", env: { account: EXTERNAL_DEV_ACCOUNT, region: "us-east-1" }, diff --git a/lib/view-access-stack.ts b/lib/view-access-stack.ts new file mode 100644 index 0000000..ca68ff9 --- /dev/null +++ b/lib/view-access-stack.ts @@ -0,0 +1,96 @@ +import * as cdk from "aws-cdk-lib"; +import * as identitystore from "aws-cdk-lib/aws-identitystore"; +import * as sso from "aws-cdk-lib/aws-sso"; +import { Construct } from "constructs"; + +const IDENTITY_CENTER_INSTANCE_ARN = + "arn:aws:sso:::instance/ssoins-722321f42ca610e4"; +const IDENTITY_STORE_ID = "d-9067ec8e26"; +const VIEW_ONLY_POLICY = "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"; + +export interface ViewAccessStackProps extends cdk.StackProps { + managementAccountId: string; + securityAccountId: string; + externalDevAccountId: string; + devAccountId: string; + prodAccountId: string; +} + +/** + * Identity Center group and permission set for view-only access across + * every organization account. + * + * ViewOnlyAccess lists and describes resources. It does not read S3 + * objects, secret values, DynamoDB items, or log contents. There is no + * inline policy and no sts:AssumeRole on OrganizationAccountAccessRole. + * + * Group membership is outside this stack. + */ +export class ViewAccessStack extends cdk.Stack { + constructor(scope: Construct, id: string, props: ViewAccessStackProps) { + super(scope, id, props); + + const group = new identitystore.CfnGroup(this, "ViewGroup", { + identityStoreId: IDENTITY_STORE_ID, + displayName: "view", + description: + "View-only across all organization accounts. No data-plane reads.", + }); + + const permissionSet = new sso.CfnPermissionSet(this, "ViewPermissionSet", { + instanceArn: IDENTITY_CENTER_INSTANCE_ARN, + name: "View", + description: + "ViewOnlyAccess in every organization account. No assume-role.", + sessionDuration: "PT8H", + managedPolicies: [VIEW_ONLY_POLICY], + }); + + this.assignment( + "ViewManagementAssignment", + permissionSet, + group, + props.managementAccountId, + ); + this.assignment( + "ViewSecurityAssignment", + permissionSet, + group, + props.securityAccountId, + ); + this.assignment( + "ViewExternalDevAssignment", + permissionSet, + group, + props.externalDevAccountId, + ); + this.assignment( + "ViewDevAssignment", + permissionSet, + group, + props.devAccountId, + ); + this.assignment( + "ViewProdAssignment", + permissionSet, + group, + props.prodAccountId, + ); + } + + private assignment( + id: string, + permissionSet: sso.CfnPermissionSet, + group: identitystore.CfnGroup, + targetId: string, + ): void { + new sso.CfnAssignment(this, id, { + instanceArn: IDENTITY_CENTER_INSTANCE_ARN, + permissionSetArn: permissionSet.attrPermissionSetArn, + principalId: group.attrGroupId, + principalType: "GROUP", + targetId, + targetType: "AWS_ACCOUNT", + }); + } +}