#!/usr/bin/env bash ############################################################################### # create-deploy-role.sh # # Creates / updates the GitHub Actions OIDC deploy role # `githubdeploy-procurement-ingest` in AWS account 011934824531 (seahaven-prod), # us-east-1, for the Sea-Haven-Industries/procurement-ingest repo (main branch). # # Part of the mgmt -> seahaven-prod migration. The mgmt-account role of the # same name is left untouched until decommission (emergency mgmt deploy path). # # GATE - DO NOT EXECUTE until BOTH of the following have passed on these # exact artifact files: # 1. GPT-4.1 cross-family review (IAM policy / trust-policy change) via # cross_review.py in the security-review repo. # 2. /sh-security-review (deep agentic pass - IaC/IAM is a gated surface). # # The SmokeInvokeLambda statement is REQUIRED, not optional: deploy.yaml runs # scripts/post-deploy-smoke.sh under this role's session (not the assumed # cdk-* roles), and the smoke gate fails closed without lambda:InvokeFunction # on exactly the two email-processor function ARNs. The mgmt-era equivalent # of this policy was applied out-of-band and documented nowhere; these # artifacts are the fix for that debt. # # Resolved facts (verified 2026-07-23, read-only): # Account : 011934824531 (seahaven-prod) # Region : us-east-1 # Qualifier : hnb659fds (AWS CDK DEFAULT - matches cdk/cdk.json) # OIDC prov : arn:aws:iam::011934824531:oidc-provider/token.actions.githubusercontent.com ############################################################################### set -euo pipefail PROFILE="seahaven-prod" ROLE_NAME="githubdeploy-procurement-ingest" POLICY_NAME="procurement-ingest-deploy" ACCOUNT_ID="011934824531" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TRUST_POLICY="file://${SCRIPT_DIR}/trust-policy.json" PERMS_POLICY="file://${SCRIPT_DIR}/permissions-policy.json" echo "==> Verifying active account for profile '${PROFILE}'..." CALLER_ACCOUNT="$(aws --profile "${PROFILE}" sts get-caller-identity --query Account --output text)" if [[ "${CALLER_ACCOUNT}" != "${ACCOUNT_ID}" ]]; then echo "ERROR: profile '${PROFILE}' resolves to account ${CALLER_ACCOUNT}, expected ${ACCOUNT_ID}. Aborting." >&2 exit 1 fi echo "==> Ensuring role '${ROLE_NAME}' exists with the correct trust policy..." if aws --profile "${PROFILE}" iam get-role --role-name "${ROLE_NAME}" >/dev/null 2>&1; then echo " Role exists - updating assume-role (trust) policy." aws --profile "${PROFILE}" iam update-assume-role-policy \ --role-name "${ROLE_NAME}" \ --policy-document "${TRUST_POLICY}" else echo " Role absent - creating." aws --profile "${PROFILE}" iam create-role \ --role-name "${ROLE_NAME}" \ --assume-role-policy-document "${TRUST_POLICY}" \ --description "GitHub Actions OIDC deploy role for Sea-Haven-Industries/procurement-ingest (main)" \ --max-session-duration 3600 \ --tags Key=project,Value=procurement-ingest Key=managed-by,Value=create-deploy-role.sh fi echo "==> Putting inline permissions policy '${POLICY_NAME}' (create-or-replace)..." aws --profile "${PROFILE}" iam put-role-policy \ --role-name "${ROLE_NAME}" \ --policy-name "${POLICY_NAME}" \ --policy-document "${PERMS_POLICY}" # Drift check: this script only reconciles the single inline policy above, so # any OTHER inline or attached policy on the role is an unreviewed grant that # diverges the live role from these artifacts. Warn-only (never auto-delete) is # deliberate: this is a shared prod-tenant account and an extra policy might be # a legitimate out-of-band grant an operator must adjudicate, not blindly strip. echo "==> Checking for unexpected policies on the role..." EXTRA_INLINE="$(aws --profile "${PROFILE}" iam list-role-policies \ --role-name "${ROLE_NAME}" --query "PolicyNames[?@!='${POLICY_NAME}']" --output text)" EXTRA_ATTACHED="$(aws --profile "${PROFILE}" iam list-attached-role-policies \ --role-name "${ROLE_NAME}" --query 'AttachedPolicies[].PolicyName' --output text)" if [[ -n "${EXTRA_INLINE}" || -n "${EXTRA_ATTACHED}" ]]; then echo " WARNING: unreviewed policies present on ${ROLE_NAME} (not in these artifacts):" >&2 [[ -n "${EXTRA_INLINE}" ]] && echo " inline: ${EXTRA_INLINE}" >&2 [[ -n "${EXTRA_ATTACHED}" ]] && echo " attached: ${EXTRA_ATTACHED}" >&2 echo " Review and remove them (delete-role-policy / detach-role-policy) if unexpected." >&2 else echo " OK - only ${POLICY_NAME} present." fi ROLE_ARN="$(aws --profile "${PROFILE}" iam get-role --role-name "${ROLE_NAME}" \ --query Role.Arn --output text)" echo "==> Done. Deploy role ready:" echo " ${ROLE_ARN}" echo " Set the GitHub repo secret AWS_DEPLOY_ROLE_ARN to this ARN (Phase 1 merge sequencing:" echo " flip the secret only when Phase 0 is complete and the migration PR is about to merge)."