diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 50d02af..db2439e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,15 +1,70 @@ name: CI + +# Parallel static-site and Terraform portions. ci-complete is the check the +# CI complete ruleset requires. The ci job remains so main branch protection +# still sees ci / ci until this repo is moved onto that ruleset. + on: pull_request: - branches: [main] + branches: [main, hotfix/**, release/**] merge_group: + push: + branches: [hotfix/**, release/**] permissions: contents: read jobs: + autofix: + if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork + uses: Sea-Haven-Industries/.github/.github/workflows/ci-autofix.yaml@0a1010e63248c9ca9f042c870eb2c579ba6b9455 # v1.0.19 + permissions: + contents: write + secrets: inherit + with: + presets: terraform + ci: - uses: Sea-Haven-Industries/.github/.github/workflows/ci-static.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11 + needs: autofix + if: always() && !cancelled() && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true') + uses: Sea-Haven-Industries/.github/.github/workflows/ci-static.yaml@0a1010e63248c9ca9f042c870eb2c579ba6b9455 # v1.0.19 with: build-command: "npx @11ty/eleventy" check-dir: "_site" + + # ci-static cancels other in-progress runs of this workflow on the same ref. + # Run after the legacy ci job so both calls can finish. + static: + needs: [autofix, ci] + if: always() && !cancelled() && needs.ci.result == 'success' && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true') + uses: Sea-Haven-Industries/.github/.github/workflows/ci-static.yaml@0a1010e63248c9ca9f042c870eb2c579ba6b9455 # v1.0.19 + with: + build-command: "npx @11ty/eleventy" + check-dir: "_site" + + terraform: + needs: autofix + if: always() && !cancelled() && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true') + uses: Sea-Haven-Industries/.github/.github/workflows/ci-terraform.yaml@0a1010e63248c9ca9f042c870eb2c579ba6b9455 # v1.0.19 + with: + terraform-version: "1.16.0" + + ci-complete: + name: ci-complete + needs: [autofix, ci, static, terraform] + if: always() && !cancelled() && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true') + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Require portions + env: + AUTOFIX: ${{ needs.autofix.result }} + CI: ${{ needs.ci.result }} + STATIC: ${{ needs.static.result }} + TERRAFORM: ${{ needs.terraform.result }} + run: | + set -euo pipefail + test "${AUTOFIX}" = success -o "${AUTOFIX}" = skipped + test "${CI}" = success + test "${STATIC}" = success + test "${TERRAFORM}" = success diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index a545917..d8c6d85 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -7,7 +7,7 @@ permissions: jobs: review: - uses: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11 + uses: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml@0a1010e63248c9ca9f042c870eb2c579ba6b9455 # v1.0.19 with: # brace-expansion OOM DoS: no in-range fix (patch only in 5.0.8; @11ty/recursive-copy # pins minimatch <10.0.3). Build-time-only exposure, adjudicated in diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 48a05b6..8a00e86 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -1,9 +1,27 @@ name: Deploy +# Static-site CD. The org reusable builds _site/, syncs the bucket root, and +# invalidates CloudFront. Terraform owns the bucket and the distribution. +# Nothing here creates an HCP run. +# +# push to main -> prod +# weekday cron -> prod (Paychex listings, no commit) +# workflow_dispatch -> prod, the main commit only +# +# Vercel previews branches other than main. + on: push: - branches: - - main + branches: [main] + paths-ignore: + # deploy.yaml is included so this transition commit does not start a + # prod deploy before the exec role can write the SSM contract. + - "terraform/**" + - "**/*.md" + - ".github/workflows/ci.yaml" + - ".github/workflows/deploy.yaml" + - ".github/workflows/labeler.yml" + - ".github/workflows/dependency-review.yml" schedule: # Weekday morning US Eastern (13:00 UTC). Rebuilds listings from Paychex # without a commit. A failed feed fetch aborts before the S3 sync. @@ -11,72 +29,20 @@ on: workflow_dispatch: permissions: - id-token: write # Required for OIDC - contents: read # Allows checkout of repo - -# Never cancel a deploy mid-flight: cancelling between the S3 sync and the -# CloudFront invalidation (or mid `--delete`) would leave the bucket in a -# half-updated state. Queue instead. -concurrency: - group: deploy-${{ github.ref }} - cancel-in-progress: false + contents: read jobs: - deploy: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v7.0.1 - - - name: Set up Node - uses: actions/setup-node@v7 - with: - node-version: "24" - cache: npm - - - name: Build site - run: | - npm ci --ignore-scripts - npm run build - # Fail closed: never let a silently-empty build reach the --delete sync. - test -f _site/index.html - test -f _site/contact/index.html - test -f _site/404.html - count=$(find _site -type f | wc -l) - if [ "$count" -lt 40 ]; then - echo "::error::build produced only $count files (expected >= 40); aborting deploy" - exit 1 - fi - echo "Build OK: $count files." - - - name: Configure AWS credentials using OIDC - uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0 - with: - role-to-assume: arn:aws:iam::011934824531:role/tf-managed/githubdeploy-seahaven-site - aws-region: us-east-1 - - - name: Sync build output to S3 - run: | - # 1) Static assets — 1-day browser cache (no filename fingerprinting yet, - # so do NOT go immutable). CloudFront /* invalidation below keeps the - # edge fresh; this only affects returning visitors' browser cache. - aws s3 sync _site/ s3://seahaven-site-prod --no-progress \ - --exclude "*.html" --exclude "*.xml" --exclude "*.txt" \ - --cache-control "public, max-age=86400" - - # 2) HTML / sitemap / robots — always revalidate so a deploy is seen immediately. - aws s3 sync _site/ s3://seahaven-site-prod --no-progress \ - --exclude "*" --include "*.html" --include "*.xml" --include "*.txt" \ - --cache-control "no-cache" - - # 3) Prune files removed from the build. This pass sets no metadata, so - # it skips already-uploaded objects (preserving the Cache-Control set - # above) and only deletes objects no longer present in _site/. - aws s3 sync _site/ s3://seahaven-site-prod --no-progress --delete - - - name: Invalidate CloudFront cache - run: | - aws cloudfront create-invalidation \ - --distribution-id E35OCA79OAJ03H \ - --paths "/*" + deploy-prod: + name: Deploy site to prod + uses: Sea-Haven-Industries/.github/.github/workflows/cd-hcp-static.yaml@0a1010e63248c9ca9f042c870eb2c579ba6b9455 # v1.0.19 + permissions: + contents: read + id-token: write + secrets: inherit + with: + environment: prod + ssm-prefix: /seahaven-site/deploy + output-dir: _site + required-paths: _site/index.html,_site/contact/index.html,_site/404.html + min-file-count: 40 + ship-gate: true diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 6eea16a..0b8a6cd 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -10,4 +10,4 @@ permissions: jobs: label: - uses: Sea-Haven-Industries/.github/.github/workflows/callable-labeler.yaml@9781774f04b824b1182ff41638687f1c01c04361 # v1.0.11 + uses: Sea-Haven-Industries/.github/.github/workflows/callable-labeler.yaml@0a1010e63248c9ca9f042c870eb2c579ba6b9455 # v1.0.19 diff --git a/README.md b/README.md index 3219781..ead5445 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,11 @@ A clean static website for Sea Haven Industries — authored as [Eleventy](https - **Eleventy (11ty)** — thin static build: one base layout + partials, no client framework. Output is plain HTML/CSS/vanilla JS. - **Design system** — CSS custom properties (DM Serif Display + Inter, `--rose: #cc3366`) - **Forms** — Basin (AJAX submission, reCAPTCHA v3, honeypot spam protection) -- **Hosting** — S3 + CloudFront, deployed via GitHub Actions OIDC (`npm run build` → sync `_site/`) +- **Hosting** — S3 + CloudFront. A merge to `main`, the weekday Paychex cron, and `workflow_dispatch` deploy production through the org static-site workflow. Other branches preview on Vercel (`vercel.json` disables Vercel deploys of `main`). + +## Infrastructure + +HCP Terraform workspace `seahaven-site-prod` applies `terraform/` when `terraform/**` changes on `main`. Speculative plans run on pull requests. The workspace writes `/seahaven-site/deploy/bucket` and `/seahaven-site/deploy/distribution-id`. GitHub Environment `prod` holds `DEPLOY_ROLE_ARN`. Exec roles `hcptf-seahaven-site` and `hcptf-seahaven-site-plan` live in the `seahaven-site-hcptf` stack. ## Develop ```bash diff --git a/terraform/hcp_iam.tf b/terraform/hcp_iam.tf deleted file mode 100644 index 93f25b5..0000000 --- a/terraform/hcp_iam.tf +++ /dev/null @@ -1,455 +0,0 @@ -# HCP plan/apply roles imported from seahaven-terraform-substrate (PLAT-146). -# Import, do not recreate. Role names stay hcptf-seahaven-site / hcptf-seahaven-site-plan. -# -# Live seahaven-hcptf-iam-management DenySelfMutation blocks DetachRolePolicy -# and PutRolePolicy on hcptf-* (including this role). Import apply sequence: -# 1. seahaven-org-baseline scripts/create-hcptf-bootstrap-roles.sh -# --account prod --allow-workspace seahaven-site-prod -# 2. Point this workspace's TFC_AWS_* at hcptf-bootstrap / -# hcptf-bootstrap-plan (workspace vars, never a project set). -# 3. One Manual apply (import + detach seahaven-hcptf-iam-management + -# put scoped inline). -# 4. Point TFC_AWS_* back at hcptf-seahaven-site / hcptf-seahaven-site-plan. -# 5. Re-run the script without --allow-workspace to pin trust back to -# iam-bootstrap-prod only. -# This stack has no Lambda execution-role boundary pin. - -import { - to = aws_iam_role.hcptf_apply - id = "hcptf-seahaven-site" -} - -import { - to = aws_iam_role.hcptf_plan - id = "hcptf-seahaven-site-plan" -} - -import { - to = aws_iam_role_policy.hcptf_apply_services - id = "hcptf-seahaven-site:seahaven-site-services" -} - -import { - to = aws_iam_role_policy.hcptf_plan_refresh - id = "hcptf-seahaven-site-plan:seahaven-site-plan-refresh" -} - -import { - to = aws_iam_role_policy_attachments_exclusive.hcptf_apply - id = "hcptf-seahaven-site" -} - -import { - to = aws_iam_role_policy_attachment.hcptf_plan_viewonly - id = "hcptf-seahaven-site-plan/arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" -} - -import { - to = aws_iam_role_policy_attachments_exclusive.hcptf_plan - id = "hcptf-seahaven-site-plan" -} - -data "aws_iam_policy_document" "hcptf_apply_trust" { - statement { - sid = "HcpApply" - effect = "Allow" - actions = ["sts:AssumeRoleWithWebIdentity"] - - principals { - type = "Federated" - identifiers = ["arn:aws:iam::${local.account_id}:oidc-provider/app.terraform.io"] - } - - condition { - test = "StringEquals" - variable = "app.terraform.io:aud" - values = ["aws.workload.identity"] - } - - condition { - test = "StringEquals" - variable = "app.terraform.io:sub" - values = [ - "organization:seahaven:project:seahaven-prod:workspace:seahaven-site-prod:run_phase:apply", - ] - } - } -} - -data "aws_iam_policy_document" "hcptf_plan_trust" { - statement { - sid = "HcpPlan" - effect = "Allow" - actions = ["sts:AssumeRoleWithWebIdentity"] - - principals { - type = "Federated" - identifiers = ["arn:aws:iam::${local.account_id}:oidc-provider/app.terraform.io"] - } - - condition { - test = "StringEquals" - variable = "app.terraform.io:aud" - values = ["aws.workload.identity"] - } - - condition { - test = "StringEquals" - variable = "app.terraform.io:sub" - values = [ - "organization:seahaven:project:seahaven-prod:workspace:seahaven-site-prod:run_phase:plan", - ] - } - } -} - -data "aws_iam_policy_document" "hcptf_scoped_iam" { - statement { - sid = "DenyCreatePolicy" - effect = "Deny" - actions = ["iam:CreatePolicy", "iam:CreatePolicyVersion"] - resources = ["*"] - } - - statement { - sid = "WriteDeployRoles" - effect = "Allow" - actions = [ - "iam:AttachRolePolicy", - "iam:DeleteRolePolicy", - "iam:DetachRolePolicy", - "iam:PutRolePolicy", - "iam:TagRole", - "iam:UntagRole", - "iam:UpdateAssumeRolePolicy", - "iam:UpdateRole", - "iam:UpdateRoleDescription", - ] - resources = ["arn:aws:iam::${local.account_id}:role/tf-managed/githubdeploy-seahaven-site"] - } - - statement { - sid = "IamReadOnly" - effect = "Allow" - actions = [ - "iam:GetPolicy", - "iam:GetPolicyVersion", - "iam:GetRole", - "iam:GetRolePolicy", - "iam:ListAttachedRolePolicies", - "iam:ListInstanceProfilesForRole", - "iam:ListPolicies", - "iam:ListPolicyVersions", - "iam:ListRolePolicies", - "iam:ListRoleTags", - "iam:ListRoles", - ] - resources = ["*"] - } - - statement { - sid = "DenySelfMutation" - effect = "Deny" - actions = [ - "iam:AttachRolePolicy", - "iam:DeleteRole", - "iam:DeleteRolePolicy", - "iam:DeleteRolePermissionsBoundary", - "iam:DetachRolePolicy", - "iam:PutRolePolicy", - "iam:PutRolePermissionsBoundary", - "iam:UpdateAssumeRolePolicy", - "iam:UpdateRole", - "iam:UpdateRoleDescription", - ] - resources = [ - "arn:aws:iam::${local.account_id}:role/hcptf-*", - "arn:aws:iam::${local.account_id}:role/github-cfn-execution-role", - "arn:aws:iam::${local.account_id}:role/githubdeploy-*", - "arn:aws:iam::${local.account_id}:role/cdk-hnb659fds-*", - "arn:aws:iam::${local.account_id}:role/OrganizationAccountAccessRole", - "arn:aws:iam::${local.account_id}:role/seahaven-*", - ] - } - - statement { - sid = "DenyBoundaryTampering" - effect = "Deny" - actions = [ - "iam:DeleteRolePermissionsBoundary", - "iam:DeleteUserPermissionsBoundary", - ] - resources = [ - "arn:aws:iam::${local.account_id}:role/*", - "arn:aws:iam::${local.account_id}:user/*", - ] - } - - statement { - sid = "DenyBoundaryPolicyEdit" - effect = "Deny" - actions = [ - "iam:CreatePolicyVersion", - "iam:DeletePolicy", - "iam:DeletePolicyVersion", - "iam:SetDefaultPolicyVersion", - ] - resources = ["arn:aws:iam::${local.account_id}:policy/seahaven-*"] - } -} - -resource "aws_iam_role_policy" "hcptf_apply_services" { - name = "seahaven-site-services" - role = aws_iam_role.hcptf_apply.id - policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = [ - "s3:*", - ] - Resource = [ - "arn:aws:s3:::seahaven-site-prod", - "arn:aws:s3:::seahaven-site-prod/*", - ] - Effect = "Allow" - Sid = "OriginBucket" - }, - { - Action = [ - "iam:GetOpenIDConnectProvider", - ] - Resource = [ - "arn:aws:iam::${local.account_id}:oidc-provider/token.actions.githubusercontent.com", - ] - Effect = "Allow" - Sid = "ReadGithubOidcProvider" - }, - { - Action = [ - "cloudfront:*", - ] - Resource = "*" - Effect = "Allow" - Sid = "CloudFrontManage" - }, - { - Condition = { - StringEquals = { - "aws:RequestTag/Project" = "seahaven-site" - } - } - Action = [ - "acm:RequestCertificate", - ] - Resource = "*" - Effect = "Allow" - Sid = "AcmCreate" - }, - { - Action = [ - "acm:ListCertificates", - "acm:ListTagsForCertificate", - ] - Resource = "*" - Effect = "Allow" - Sid = "AcmList" - }, - { - Condition = { - StringEquals = { - "aws:ResourceTag/Project" = "seahaven-site" - } - } - Action = [ - "acm:DescribeCertificate", - "acm:GetCertificate", - "acm:DeleteCertificate", - "acm:AddTagsToCertificate", - "acm:RemoveTagsFromCertificate", - "acm:RenewCertificate", - ] - Resource = "*" - Effect = "Allow" - Sid = "AcmManageTagged" - }, - { - Action = [ - "ssm:GetParameter", - "ssm:GetParameters", - ] - Resource = [ - "arn:aws:ssm:us-east-1:${local.account_id}:parameter/seahaven/waf/app-web-acl-arn", - ] - Effect = "Allow" - Sid = "ReadAppWebAclSsm" - }, - { - Action = [ - "wafv2:GetWebACL", - "wafv2:GetWebACLForResource", - "wafv2:ListWebACLs", - "wafv2:ListResourcesForWebACL", - ] - Resource = "*" - Effect = "Allow" - Sid = "ReadWafWebAcl" - }, - ] - }) -} - -resource "aws_iam_role_policy" "hcptf_plan_refresh" { - name = "seahaven-site-plan-refresh" - role = aws_iam_role.hcptf_plan.id - policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = [ - "iam:GetRole", - "iam:GetRolePolicy", - "iam:ListRolePolicies", - "iam:ListAttachedRolePolicies", - "iam:ListRoleTags", - ] - Resource = [ - "arn:aws:iam::${local.account_id}:role/tf-managed/githubdeploy-seahaven-site", - "arn:aws:iam::${local.account_id}:role/hcptf-seahaven-site", - "arn:aws:iam::${local.account_id}:role/hcptf-seahaven-site-plan", - ] - Effect = "Allow" - Sid = "RefreshDeployRole" - }, - { - Action = [ - "iam:GetOpenIDConnectProvider", - ] - Resource = [ - "arn:aws:iam::${local.account_id}:oidc-provider/token.actions.githubusercontent.com", - ] - Effect = "Allow" - Sid = "RefreshGithubOidcProvider" - }, - { - Action = [ - "iam:GetPolicy", - "iam:GetPolicyVersion", - ] - Resource = "*" - Effect = "Allow" - Sid = "RefreshManagedPolicies" - }, - { - Action = [ - "s3:Get*", - "s3:ListBucket", - ] - Resource = [ - "arn:aws:s3:::seahaven-site-prod", - "arn:aws:s3:::seahaven-site-prod/*", - ] - Effect = "Allow" - Sid = "RefreshOriginBucket" - }, - { - Action = [ - "cloudfront:Get*", - "cloudfront:List*", - ] - Resource = "*" - Effect = "Allow" - Sid = "RefreshCloudFront" - }, - { - Action = [ - "cloudfront:DescribeFunction", - ] - Resource = [ - "arn:aws:cloudfront::${local.account_id}:function/seahaven-site-prod-directory-index", - ] - Effect = "Allow" - Sid = "RefreshCloudFrontFunction" - }, - { - Action = [ - "acm:DescribeCertificate", - "acm:ListCertificates", - "acm:ListTagsForCertificate", - "acm:GetCertificate", - ] - Resource = "*" - Effect = "Allow" - Sid = "RefreshAcm" - }, - { - Action = [ - "ssm:GetParameter", - "ssm:GetParameters", - ] - Resource = [ - "arn:aws:ssm:us-east-1:${local.account_id}:parameter/seahaven/waf/app-web-acl-arn", - ] - Effect = "Allow" - Sid = "RefreshAppWebAclSsm" - }, - { - Action = [ - "wafv2:GetWebACL", - "wafv2:ListWebACLs", - ] - Resource = "*" - Effect = "Allow" - Sid = "RefreshWafWebAcl" - }, - ] - }) -} - -resource "aws_iam_role" "hcptf_apply" { - name = "hcptf-seahaven-site" - assume_role_policy = data.aws_iam_policy_document.hcptf_apply_trust.json - max_session_duration = 3600 - - tags = { - Project = "seahaven-site" - Owner = "adam@seahavenind.com" - ManagedBy = "terraform" - } -} - -# Empty exclusive set keeps seahaven-hcptf-iam-management detached. -resource "aws_iam_role_policy_attachments_exclusive" "hcptf_apply" { - role_name = aws_iam_role.hcptf_apply.name - policy_arns = [] -} - -resource "aws_iam_role" "hcptf_plan" { - name = "hcptf-seahaven-site-plan" - assume_role_policy = data.aws_iam_policy_document.hcptf_plan_trust.json - max_session_duration = 3600 - - tags = { - Project = "seahaven-site" - Owner = "adam@seahavenind.com" - ManagedBy = "terraform" - } -} - -resource "aws_iam_role_policy_attachment" "hcptf_plan_viewonly" { - role = aws_iam_role.hcptf_plan.name - policy_arn = "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess" -} - -resource "aws_iam_role_policy_attachments_exclusive" "hcptf_plan" { - role_name = aws_iam_role.hcptf_plan.name - policy_arns = [ - aws_iam_role_policy_attachment.hcptf_plan_viewonly.policy_arn, - ] -} - -resource "aws_iam_role_policy" "hcptf_scoped_iam" { - name = "scoped-iam-management" - role = aws_iam_role.hcptf_apply.id - policy = data.aws_iam_policy_document.hcptf_scoped_iam.json -} diff --git a/terraform/iam_github_deploy.tf b/terraform/iam_github_deploy.tf index bcdf0e5..eea569b 100644 --- a/terraform/iam_github_deploy.tf +++ b/terraform/iam_github_deploy.tf @@ -1,5 +1,8 @@ data "aws_iam_policy_document" "github_deploy_assume" { + # Legacy branch trust. Remove after one deploy through cd-hcp-static has + # succeeded. The branch subject stays until EnvironmentProd is applied. statement { + sid = "LegacyBranch" effect = "Allow" actions = ["sts:AssumeRoleWithWebIdentity"] @@ -20,25 +23,56 @@ data "aws_iam_policy_document" "github_deploy_assume" { values = ["repo:${var.github_repo}:ref:refs/heads/${var.github_deploy_branch}"] } } + + statement { + sid = "EnvironmentProd" + effect = "Allow" + actions = ["sts:AssumeRoleWithWebIdentity"] + + principals { + type = "Federated" + identifiers = [data.aws_iam_openid_connect_provider.github.arn] + } + + condition { + test = "StringEquals" + variable = "token.actions.githubusercontent.com:aud" + values = ["sts.amazonaws.com"] + } + + condition { + test = "StringEquals" + variable = "token.actions.githubusercontent.com:sub" + values = ["repo:${var.github_repo}:environment:prod"] + } + + condition { + test = "StringLike" + variable = "token.actions.githubusercontent.com:job_workflow_ref" + values = ["Sea-Haven-Industries/.github/.github/workflows/cd-hcp-static.yaml@*"] + } + } +} + +resource "aws_iam_policy" "github_deploy_boundary" { + name = "seahaven-site-githubdeploy-boundary" + path = "/tf-managed/" + description = "Permissions boundary for githubdeploy-seahaven-site" + policy = data.aws_iam_policy_document.github_deploy.json + + # Request tag the hcptf apply role requires before it may CreatePolicy. + tags = { + BoundaryFor = "githubdeploy-seahaven-site" + } } resource "aws_iam_role" "github_deploy" { name = local.deploy_role path = "/tf-managed/" - description = "GitHub Actions content-deploy role for ${var.github_repo}@${var.github_deploy_branch}" + description = "GitHub Actions content-deploy role for ${var.github_repo} Environment prod" assume_role_policy = data.aws_iam_policy_document.github_deploy_assume.json + permissions_boundary = aws_iam_policy.github_deploy_boundary.arn max_session_duration = 3600 - - # Not a Lambda execution role. Config omits permissions_boundary so a later - # apply will not PutRolePermissionsBoundary the Lambda ceiling back. Live still - # has seahaven-lambda-execution-boundary; omitting without ignore_changes would - # plan DeleteRolePermissionsBoundary, which hcptf-seahaven-site is denied - # (DenyBoundaryTampering). Ignore the attribute so this apply does not touch - # the ceiling. An administrator deletes the live attachment, then a follow-up - # drops this lifecycle after refresh-only updates state to null. - lifecycle { - ignore_changes = [permissions_boundary] - } } data "aws_iam_policy_document" "github_deploy" { @@ -70,10 +104,21 @@ data "aws_iam_policy_document" "github_deploy" { effect = "Allow" actions = [ "cloudfront:CreateInvalidation", + "cloudfront:GetDistribution", "cloudfront:GetInvalidation", ] resources = [aws_cloudfront_distribution.site.arn] } + + statement { + sid = "ReadDeployContract" + effect = "Allow" + actions = [ + "ssm:GetParameter", + "ssm:GetParameters", + ] + resources = ["arn:aws:ssm:${var.aws_region}:${local.account_id}:parameter/seahaven-site/deploy/*"] + } } resource "aws_iam_role_policy" "github_deploy" { diff --git a/terraform/removed.tf b/terraform/removed.tf new file mode 100644 index 0000000..9beb0a5 --- /dev/null +++ b/terraform/removed.tf @@ -0,0 +1,67 @@ +# hcptf-seahaven-site and hcptf-seahaven-site-plan moved to the +# seahaven-site-hcptf stack in seahaven-org-baseline (PLAT-225). +# Forget them here. Do not delete the live roles. + +removed { + from = aws_iam_role.hcptf_apply + + lifecycle { + destroy = false + } +} + +removed { + from = aws_iam_role.hcptf_plan + + lifecycle { + destroy = false + } +} + +removed { + from = aws_iam_role_policy.hcptf_apply_services + + lifecycle { + destroy = false + } +} + +removed { + from = aws_iam_role_policy.hcptf_scoped_iam + + lifecycle { + destroy = false + } +} + +removed { + from = aws_iam_role_policy.hcptf_plan_refresh + + lifecycle { + destroy = false + } +} + +removed { + from = aws_iam_role_policy_attachment.hcptf_plan_viewonly + + lifecycle { + destroy = false + } +} + +removed { + from = aws_iam_role_policy_attachments_exclusive.hcptf_apply + + lifecycle { + destroy = false + } +} + +removed { + from = aws_iam_role_policy_attachments_exclusive.hcptf_plan + + lifecycle { + destroy = false + } +} diff --git a/terraform/ssm.tf b/terraform/ssm.tf new file mode 100644 index 0000000..cf8b9bd --- /dev/null +++ b/terraform/ssm.tf @@ -0,0 +1,14 @@ +# Names the static-site deploy workflow reads. Terraform does not manage the +# objects in the bucket, so a content deploy does not change this plan. + +resource "aws_ssm_parameter" "deploy_bucket" { + name = "/seahaven-site/deploy/bucket" + type = "String" + value = aws_s3_bucket.origin.bucket +} + +resource "aws_ssm_parameter" "deploy_distribution_id" { + name = "/seahaven-site/deploy/distribution-id" + type = "String" + value = aws_cloudfront_distribution.site.id +}