d28630d1f1
---ci--- project: acdl phase: 8 milestone: v1.1 status: plan-as-execute persona: lead-developer task: T-8.8 requirements.covered: [REQ-23] ---/ci--- Wave 5: execute the bootstrap against real AWS + fix verify script. - Created S3 bucket acdl-tfstate-581513795199-us-east-1 (versioning enabled) + DynamoDB table acdl-outbox (PAY_PER_REQUEST, PK contractId, SK eventType#eventTs) via create_state_backend.py. - Created IAM user acdl-spike-runner + scoped inline policy (DenyEverythingElse) + initial key via create_iam_user.py. - Rotated the spike key via rotate_spike_key.sh: old key deleted, new key in gitignored .env.secrets (chmod 600). - verify_phase08.sh fixes: (a) heredoc python instead of -c to avoid bash quoting issues; (b) Check 4 uses the bootstrap root key to inspect IAM (the spike key is least-privilege and cannot iam:GetUser - that itself confirms the policy denies non-granted actions); (c) get_user_policy returns PolicyDocument as a dict, not a URL-encoded string in this boto3 version - handle both. - VERIFIED: caller identity is acdl-spike-runner (not root), S3 + DDB + IAM user + Deny-everything-else policy all present, .env.secrets + .bootstrap_state.json gitignored. D-034 closure: user must manually rotate the root key in the AWS IAM console now (the bootstrap root key has served its one-shot purpose).
80 lines
3.5 KiB
Bash
Executable File
80 lines
3.5 KiB
Bash
Executable File
#!/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)" |