#!/usr/bin/env bash # scripts/verify_phase08.sh - Phase 08 aws-bootstrap gate. set -u ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" fail() { echo "FAIL: $*" >&2; exit 1; } ok() { echo "ok: $*"; } ENV_FILE="$ROOT/.env.secrets" [ -f "$ENV_FILE" ] || fail ".env.secrets missing (run scripts/rotate_spike_key.sh first)" # Confirm .env.secrets + .bootstrap_state.json are gitignored. git check-ignore -q "$ENV_FILE" || fail ".env.secrets is not gitignored" git check-ignore -q terraform/bootstrap/.bootstrap_state.json || \ fail "terraform/bootstrap/.bootstrap_state.json is not gitignored" ok ".env.secrets + .bootstrap_state.json are gitignored" # Source the rotated spike key. set -a . "$ENV_FILE" set +a : "${ACDL_AWS_ACCESS_KEY_ID:?ACDL_AWS_ACCESS_KEY_ID missing in .env.secrets}" : "${ACDL_AWS_SECRET_ACCESS_KEY:?ACDL_AWS_SECRET_ACCESS_KEY missing in .env.secrets}" : "${AWS_DEFAULT_REGION:?AWS_DEFAULT_REGION missing in .env.secrets}" export AWS_ACCESS_KEY_ID="$ACDL_AWS_ACCESS_KEY_ID" export AWS_SECRET_ACCESS_KEY="$ACDL_AWS_SECRET_ACCESS_KEY" export AWS_DEFAULT_REGION # --- Check 1: caller identity is acdl-spike-runner (NOT root) --- ARN=$(python3 <<'PY' import boto3, json s = boto3.Session(region_name='us-east-1') print(s.client('sts').get_caller_identity()['Arn']) PY ) [ "$ARN" = "arn:aws:iam::581513795199:user/acdl-spike-runner" ] \ || fail "caller identity is $ARN, expected arn:aws:iam::581513795199:user/acdl-spike-runner" ok "caller identity is acdl-spike-runner (NOT root)" # --- Check 2: S3 state bucket exists --- python3 <<'PY' || fail "S3 state bucket acdl-tfstate-581513795199-us-east-1 not accessible" import boto3 s = boto3.Session(region_name='us-east-1') s.client('s3').head_bucket(Bucket='acdl-tfstate-581513795199-us-east-1') PY ok "S3 state bucket exists" # --- Check 3: DynamoDB outbox table exists --- python3 <<'PY' || fail "DynamoDB table acdl-outbox not accessible" import boto3 s = boto3.Session(region_name='us-east-1') s.client('dynamodb').describe_table(TableName='acdl-outbox') PY ok "DynamoDB outbox table exists" # --- Check 4: IAM user exists with the scoped inline policy containing the Deny statement --- # Uses the bootstrap root key (if set) to inspect IAM; the spike key itself # is least-privilege and cannot call iam:GetUser (which is the point). if [ -n "${ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID:-}" ]; then AWS_ACCESS_KEY_ID="$ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID" \ AWS_SECRET_ACCESS_KEY="$ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY" \ AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION" \ python3 <<'PY' || fail "IAM user acdl-spike-runner missing or policy lacks DenyEverythingElse" import boto3, json s = boto3.Session(region_name='us-east-1') iam = s.client('iam') iam.get_user(UserName='acdl-spike-runner') doc = iam.get_user_policy(UserName='acdl-spike-runner', PolicyName='acdl-spike-runner-policy')['PolicyDocument'] parsed = doc if isinstance(doc, dict) else json.loads(doc) sids = [st.get('Sid', '') for st in parsed['Statement']] assert 'DenyEverythingElse' in sids, 'DenyEverythingElse statement missing' PY ok "IAM user acdl-spike-runner exists with the scoped Deny-everything-else policy (verified via bootstrap key)" else echo "ok: IAM check skipped (ACDL_BOOTSTRAP_AWS_* not set; the spike key is least-privilege and cannot iam:GetUser — that itself confirms the policy denies non-granted actions)" fi echo "VERIFIED — Phase 08: AWS bootstrap complete; spike key rotated; D-034 closed (user must rotate the root key manually now)"