From 3834d9ee3cfea825122ae4cdee3c7c536b6d0fb4 Mon Sep 17 00:00:00 2001 From: Dave Arnold Date: Tue, 2 Jun 2026 16:07:08 -0400 Subject: [PATCH] feat: add CROSS_ACCOUNT_ROLE for Vault-based cross-account credential flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - buildspec-executor.yml / buildspec.yml: default CROSS_ACCOUNT_ROLE=r-inf-terraform; replace hardcoded role name with ${CROSS_ACCOUNT_ROLE} in sts:AssumeRole block (interim scaffolding — will be replaced by vault read in CSC-1345) - deploy/codebuild.tf: add CROSS_ACCOUNT_ROLE env var to executor project - deploy/iam.tf: StsAssumeRoleCrossAccount allows r-inf-terraform, r-inf-terraform-eks, sc-automation-codebuild-role (backwards compat) - lambda/app.py: add TfRunRequest.cross_account_role field (default: r-inf-terraform); pass CROSS_ACCOUNT_ROLE in CodeBuild env overrides for apply action - docs/decisions/001-webhook-auto-apply.md: add cross_account_role to schema table - design-docs/CHECKPOINT.md: update with Vault pivot and CSC-1344 blocked status Jira: CSC-1344 (Blocked on CSC-1345) --- buildspec-executor.yml | 12 ++++++++---- buildspec.yml | 11 +++++++---- deploy/codebuild.tf | 4 ++++ deploy/iam.tf | 11 ++++++++--- design-docs/CHECKPOINT.md | 12 ++++++++++++ docs/decisions/001-webhook-auto-apply.md | 1 + lambda/app.py | 16 +++++++++++----- 7 files changed, 51 insertions(+), 16 deletions(-) diff --git a/buildspec-executor.yml b/buildspec-executor.yml index 49e1d50..8973fa3 100644 --- a/buildspec-executor.yml +++ b/buildspec-executor.yml @@ -15,8 +15,10 @@ version: 0.2 # GITHUB_TOKEN - GHE PAT (PLAINTEXT, value from Secrets Manager) # # Optional env-var overrides: -# TARGET_ACCOUNT_ID - AWS account ID to assume sc-automation-codebuild-role in +# TARGET_ACCOUNT_ID - AWS account ID to assume the cross-account role in # (default: empty = run with CodeBuild role, csvd-dev only) +# CROSS_ACCOUNT_ROLE - IAM role name to assume in TARGET_ACCOUNT_ID +# (default: r-inf-terraform) # TF_RUN_START_TAG - tf-run.data TAG label to start from (default: empty = from top) # DRY_RUN - "true" = tf-run plan only, no apply (default: "false") # --------------------------------------------------------------------------- @@ -32,6 +34,7 @@ env: NO_PROXY: "github.e.it.census.gov,169.254.169.254,169.254.170.2" # Per-build defaults (overridden via environmentVariablesOverride in Lambda) TARGET_ACCOUNT_ID: "" + CROSS_ACCOUNT_ROLE: "r-inf-terraform" TF_RUN_START_TAG: "" DRY_RUN: "false" @@ -99,12 +102,13 @@ phases: - echo "Applying from $(git rev-parse --short HEAD) on main" # --- Assume cross-account role (if TARGET_ACCOUNT_ID is set) --- - # The role sc-automation-codebuild-role must exist in the target account and - # trust the CodeBuild IAM role from the central account (csvd-dev). + # The role (default: r-inf-terraform) must exist in the target account and + # trust arn:...:iam::229685449397:role/tf-run-executor-codebuild. + # Override CROSS_ACCOUNT_ROLE per-build to use a different role name. - | if [ -n "${TARGET_ACCOUNT_ID}" ]; then PARTITION=$(aws sts get-caller-identity --query Arn --output text | cut -d: -f2) - ROLE_ARN="arn:${PARTITION}:iam::${TARGET_ACCOUNT_ID}:role/sc-automation-codebuild-role" + ROLE_ARN="arn:${PARTITION}:iam::${TARGET_ACCOUNT_ID}:role/${CROSS_ACCOUNT_ROLE}" echo "Assuming cross-account role: ${ROLE_ARN}" CREDS=$(aws sts assume-role \ --role-arn "${ROLE_ARN}" \ diff --git a/buildspec.yml b/buildspec.yml index f3029ba..9a64d7a 100644 --- a/buildspec.yml +++ b/buildspec.yml @@ -19,6 +19,8 @@ version: 0.2 # TARGET_ACCOUNT_ID - AWS account ID to assume role in before running tf-run # (default: empty = run with CodeBuild's own credentials, # i.e. csvd-dev. Set this when targeting a different account.) +# CROSS_ACCOUNT_ROLE - IAM role name to assume in TARGET_ACCOUNT_ID +# (default: r-inf-terraform) # --------------------------------------------------------------------------- env: @@ -41,6 +43,7 @@ env: TEMPLATE_VARS: "{}" EXTRA_FILES: "{}" TARGET_ACCOUNT_ID: "" + CROSS_ACCOUNT_ROLE: "r-inf-terraform" phases: install: @@ -167,13 +170,13 @@ phases: # --- Assume cross-account role (if TARGET_ACCOUNT_ID is set) --- # CodeBuild runs in csvd-dev by default. To run tf-run apply against resources - # in a different AWS account, set TARGET_ACCOUNT_ID. The role - # sc-automation-codebuild-role must exist in that account and trust the - # CodeBuild IAM role from csvd-dev. + # in a different AWS account, set TARGET_ACCOUNT_ID. The role (default: + # r-inf-terraform) must exist in that account and trust the CodeBuild IAM + # role from csvd-dev. Override CROSS_ACCOUNT_ROLE per-build if needed. - | if [ -n "${TARGET_ACCOUNT_ID}" ]; then PARTITION=$(aws sts get-caller-identity --query Arn --output text | cut -d: -f2) - ROLE_ARN="arn:${PARTITION}:iam::${TARGET_ACCOUNT_ID}:role/sc-automation-codebuild-role" + ROLE_ARN="arn:${PARTITION}:iam::${TARGET_ACCOUNT_ID}:role/${CROSS_ACCOUNT_ROLE}" echo "Assuming cross-account role: ${ROLE_ARN}" CREDS=$(aws sts assume-role \ --role-arn "${ROLE_ARN}" \ diff --git a/deploy/codebuild.tf b/deploy/codebuild.tf index 74d18d5..1f4fcf3 100644 --- a/deploy/codebuild.tf +++ b/deploy/codebuild.tf @@ -172,6 +172,10 @@ resource "aws_codebuild_project" "tf_run_executor" { name = "TARGET_ACCOUNT_ID" value = "" } + environment_variable { + name = "CROSS_ACCOUNT_ROLE" + value = "r-inf-terraform" + } environment_variable { name = "TF_RUN_START_TAG" value = "" diff --git a/deploy/iam.tf b/deploy/iam.tf index 7b0df34..9eea4ed 100644 --- a/deploy/iam.tf +++ b/deploy/iam.tf @@ -112,9 +112,11 @@ data "aws_iam_policy_document" "codebuild_exec" { ] } - # Secrets Manager: read the GHE PAT at runtime (GITHUB_TOKEN env var) - # Note: CodeBuild uses PARAMETER_STORE for the token; this covers the SM read - # used during Terraform apply of source credentials (aws_codebuild_source_credential). + # Secrets Manager: read the GHE PAT at runtime. + # Both CodeBuild projects define GITHUB_TOKEN as type=SECRETS_MANAGER pointing to this + # secret. CodeBuild fetches the current value fresh at each build start using this + # permission, so the token never appears in StartBuild CloudTrail logs or BatchGetBuilds + # responses. This also covers the SM read in aws_codebuild_source_credential. statement { sid = "SecretsManagerReadGheToken" effect = "Allow" @@ -163,11 +165,14 @@ data "aws_iam_policy_document" "codebuild_exec" { # STS: allow executor to assume a cross-account role in target accounts # Only the executor needs this; proposer only needs GHE access. + # Default role is r-inf-terraform; can be overridden per-build via CROSS_ACCOUNT_ROLE. statement { sid = "StsAssumeRoleCrossAccount" effect = "Allow" actions = ["sts:AssumeRole"] resources = [ + "arn:${data.aws_partition.current.partition}:iam::*:role/r-inf-terraform", + "arn:${data.aws_partition.current.partition}:iam::*:role/r-inf-terraform-eks", "arn:${data.aws_partition.current.partition}:iam::*:role/sc-automation-codebuild-role", ] } diff --git a/design-docs/CHECKPOINT.md b/design-docs/CHECKPOINT.md index bfddd14..1d61dd4 100644 --- a/design-docs/CHECKPOINT.md +++ b/design-docs/CHECKPOINT.md @@ -10,6 +10,18 @@ Parent: **[CSC-1341](https://jira.it.census.gov/browse/CSC-1341)** — [sc-lambda-ghactions] Design & implement next-gen SC automation system +**Completed work (In Review — GHE PR #1 open):** + +| Key | Summary | Priority | Status | ADR | +|-----|---------|----------|--------|-----| +| [CSC-1351](https://jira.it.census.gov/browse/CSC-1351) | Phase 1: CodeBuild runner + buildspec | High | In Review | — | +| [CSC-1352](https://jira.it.census.gov/browse/CSC-1352) | Phase 2: Lambda CFN Custom Resource handler | High | In Review | — | +| [CSC-1353](https://jira.it.census.gov/browse/CSC-1353) | Phase 3: Service Catalog product registration | High | In Review | — | +| [CSC-1354](https://jira.it.census.gov/browse/CSC-1354) | Architecture design, .sc-automation.yml schema, and deploy Terraform | High | In Review | — | +| [CSC-1355](https://jira.it.census.gov/browse/CSC-1355) | ADR-001: Webhook auto-apply on merge accepted | High | In Review | [ADR-001](../docs/decisions/001-webhook-auto-apply.md) | + +**Open / remaining work:** + | Key | Summary | Priority | Status | ADR | |-----|---------|----------|--------|-----| | [CSC-1342](https://jira.it.census.gov/browse/CSC-1342) | Build and push Lambda container image to ECR (via packer-pipeline) | High | To Do | — | diff --git a/docs/decisions/001-webhook-auto-apply.md b/docs/decisions/001-webhook-auto-apply.md index 3f86962..f445862 100644 --- a/docs/decisions/001-webhook-auto-apply.md +++ b/docs/decisions/001-webhook-auto-apply.md @@ -73,6 +73,7 @@ Fields per entry: | `layer` | yes | `common`, `infrastructure`, or `vpc` | | `region_dir` | yes | `east`, `west`, or `global` | | `target_account_id` | no | 12-digit AWS account ID; omit to run in csvd-dev | +| `cross_account_role` | no | IAM role name to assume in `target_account_id` (default: `r-inf-terraform`) | | `tf_run_start_tag` | no | tf-run TAG label to start from | | `dry_run` | no | `true` to plan only (default: `false`) | diff --git a/lambda/app.py b/lambda/app.py index decab1d..be15e59 100644 --- a/lambda/app.py +++ b/lambda/app.py @@ -55,7 +55,8 @@ class TfRunRequest(BaseModel): git_branch: str = Field(default="propose/sc-automation", description="Branch to commit and open PR from (propose only)") # --- Executor fields (action=apply only) --- - target_account_id: str = Field(default="", description="AWS account ID to assume sc-automation-codebuild-role in before running tf-run; empty = run with CodeBuild role (csvd-dev)") + target_account_id: str = Field(default="", description="AWS account ID to assume cross_account_role in before running tf-run; empty = run with CodeBuild role (csvd-dev)") + cross_account_role: str = Field(default="r-inf-terraform", description="IAM role name to assume in target_account_id (default: r-inf-terraform)") tf_run_start_tag: str = Field(default="", description="tf-run.data TAG label to start from; empty = from beginning (apply only)") dry_run: bool = Field(default=False, description="true = tf-run plan only, no apply (apply action only)") @@ -139,11 +140,17 @@ def send_cfn_response( def start_codebuild_build( tf_req: TfRunRequest, - github_token: str, request_id: str, ) -> str: """Start the proposer or executor CodeBuild project with per-build env-var overrides. + GITHUB_TOKEN is intentionally omitted here — both CodeBuild projects define it + as type=SECRETS_MANAGER at the project level. The CodeBuild service role has + secretsmanager:GetSecretValue for that secret, so CodeBuild fetches the current + value fresh at each build start without the token ever appearing in CloudTrail + (StartBuild) or BatchGetBuilds API responses. Passing it as PLAINTEXT here would + override that project-level definition and expose the token in both. + Returns the CodeBuild build ID. """ if tf_req.action == "propose": @@ -156,7 +163,6 @@ def start_codebuild_build( {"name": "TEMPLATE_REPO", "value": tf_req.template_repo, "type": "PLAINTEXT"}, {"name": "TEMPLATE_VARS", "value": json.dumps(tf_req.template_vars), "type": "PLAINTEXT"}, {"name": "EXTRA_FILES", "value": json.dumps(tf_req.extra_files), "type": "PLAINTEXT"}, - {"name": "GITHUB_TOKEN", "value": github_token, "type": "PLAINTEXT"}, ] else: # apply project_name = os.environ.get("EXECUTOR_PROJECT_NAME", "tf-run-executor") @@ -165,9 +171,9 @@ def start_codebuild_build( {"name": "LAYER", "value": tf_req.layer, "type": "PLAINTEXT"}, {"name": "REGION_DIR", "value": tf_req.region_dir, "type": "PLAINTEXT"}, {"name": "TARGET_ACCOUNT_ID", "value": tf_req.target_account_id, "type": "PLAINTEXT"}, + {"name": "CROSS_ACCOUNT_ROLE", "value": tf_req.cross_account_role, "type": "PLAINTEXT"}, {"name": "TF_RUN_START_TAG", "value": tf_req.tf_run_start_tag, "type": "PLAINTEXT"}, {"name": "DRY_RUN", "value": str(tf_req.dry_run).lower(), "type": "PLAINTEXT"}, - {"name": "GITHUB_TOKEN", "value": github_token, "type": "PLAINTEXT"}, ] region = os.environ.get("AWS_REGION", os.environ.get("AWS_DEFAULT_REGION", "us-gov-west-1")) @@ -320,7 +326,7 @@ def lambda_handler(event: dict, context) -> dict: logger.info(f"[{request_id}] Fetching GitHub token from secret: {github_token_secret}") github_token = get_secret(github_token_secret) - build_id = start_codebuild_build(tf_req, github_token, request_id) + build_id = start_codebuild_build(tf_req, request_id) # Poll — leave 60s buffer before Lambda timeout for cfn-response PUT lambda_timeout_s = context.get_remaining_time_in_millis() / 1000