mirror of
https://github.com/Sea-Haven-Industries/procurement-ingest.git
synced 2026-09-30 07:13:13 +00:00
* iac(access): temporary shoc-assessment-dynamo-reader role for Luby initial assessment 30-day, read-only (GetItem/Query/Scan/DescribeTable) cross-account role in seahaven-prod trusting seahaven-external-dev, trust-policy hard expiry 2026-08-23. Steady state remains procurement-api + webhook; teardown script included. * harden(access): resolve sh-security-review findings on assessment reader C1 (confirmed medium): DateLessThan expiry condition duplicated into both permissions statements so in-flight sessions die at the deadline, not +1h. C2 (confirmed medium): teardown now strips ALL inline/attached policies and instance profiles before DeleteRole (kill-switch semantics restored, idempotent), emergency-revocation section added to README. Cheap hardenings: CDPATH-immune SCRIPT_DIR, MaxSessionDuration re-asserted on update path, data-handling expectations documented. * harden(access): address Open SWE review on #141 - Trust now requires ArnLike aws:PrincipalArn on the Identity Center role path (human SSO sessions only; string condition survives permission-set reprovisioning, unlike a role-ARN Principal pin) - README extend instructions cover BOTH expiry sites (trust + permissions) - Create script logs caller ARN + timestamp and validates the policy's KMS ARN against SSM /seahaven/dynamodb/cmk-arn before applying
116 lines
6.1 KiB
Bash
Executable file
116 lines
6.1 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
###############################################################################
|
|
# create-assessment-reader.sh
|
|
#
|
|
# Creates / updates the TEMPORARY cross-account read role
|
|
# `shoc-assessment-dynamo-reader` in AWS account 011934824531 (seahaven-prod),
|
|
# us-east-1, for the Luby team's initial data assessment from
|
|
# seahaven-external-dev (396287094661).
|
|
#
|
|
# TEMPORARY BY DESIGN:
|
|
# - Hard expiry 2026-08-23T00:00:00Z enforced in BOTH layers: the trust
|
|
# policy (new AssumeRole calls fail) and every permissions statement
|
|
# (in-flight sessions lose data access at the same instant), so access
|
|
# dies at the deadline even if teardown never runs.
|
|
# - Steady-state SHOC access is the procurement-api SigV4 read API plus the
|
|
# shoc-webhook emitter, NOT this role. Tear this down (or let it expire)
|
|
# once the assessment is done; extend only by a deliberate edit to
|
|
# trust-policy.json re-run through the review gates.
|
|
#
|
|
# GATE - DO NOT EXECUTE until BOTH of the following have passed on these
|
|
# exact artifact files:
|
|
# 1. GPT-4.1 cross-family review (IAM trust/permissions change) via
|
|
# cross_review.py in the security-review repo.
|
|
# 2. /sh-security-review (deep agentic pass - IaC/IAM is a gated surface).
|
|
#
|
|
# Design notes (deliberate, do not "fix" without re-review):
|
|
# - Account-root trust (396287094661:root), NOT a pinned role: the assessors
|
|
# are the Luby humans on SSO Admin sessions in external-dev, and every
|
|
# principal in that account is Luby-controlled + SCP/boundary-contained.
|
|
# - NO sts:ExternalId condition: console Switch-Role cannot pass one, and
|
|
# there is no third-party deputy in this human-driven path. (The old
|
|
# mgmt-account `shoc-dynamo-reader` used ExternalId because it served an
|
|
# EC2 role, i.e. an actual service deputy.)
|
|
# - dynamodb:DescribeTable is included beyond the requested
|
|
# GetItem/Query/Scan because the DynamoDB console item explorer and most
|
|
# SDK table bindings require it (metadata only).
|
|
# - kms:Decrypt is ViaService-pinned to DynamoDB: the three po-ingest tables
|
|
# are CMK-encrypted (seahaven-dynamodb-cmk); the WO tables are
|
|
# AWS-owned-key encrypted and need no KMS grant.
|
|
# - Max session 3600s: sessions from external-dev are role-chained (SSO ->
|
|
# Admin role -> this role), and chained sessions cap at 1h regardless.
|
|
###############################################################################
|
|
|
|
set -euo pipefail
|
|
|
|
PROFILE="seahaven-prod"
|
|
ROLE_NAME="shoc-assessment-dynamo-reader"
|
|
POLICY_NAME="shoc-assessment-dynamo-read"
|
|
ACCOUNT_ID="011934824531"
|
|
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
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
|
|
CALLER_ARN="$(aws --profile "${PROFILE}" sts get-caller-identity --query Arn --output text)"
|
|
echo " Audit: run by ${CALLER_ARN} at $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
|
|
echo "==> Validating the KMS key ARN in permissions-policy.json against SSM /seahaven/dynamodb/cmk-arn..."
|
|
LIVE_CMK_ARN="$(aws --profile "${PROFILE}" ssm get-parameter \
|
|
--name /seahaven/dynamodb/cmk-arn --query Parameter.Value --output text)"
|
|
if ! grep -qF "\"${LIVE_CMK_ARN}\"" "${SCRIPT_DIR}/permissions-policy.json"; then
|
|
echo "ERROR: permissions-policy.json does not reference the live DynamoDB CMK (${LIVE_CMK_ARN})." >&2
|
|
echo " Copy/paste drift would make CMK-table reads fail silently at runtime. Aborting." >&2
|
|
exit 1
|
|
fi
|
|
echo " OK - policy references the live CMK."
|
|
|
|
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 + re-asserting session cap."
|
|
aws --profile "${PROFILE}" iam update-assume-role-policy \
|
|
--role-name "${ROLE_NAME}" \
|
|
--policy-document "${TRUST_POLICY}"
|
|
aws --profile "${PROFILE}" iam update-role \
|
|
--role-name "${ROLE_NAME}" \
|
|
--max-session-duration 3600
|
|
else
|
|
echo " Role absent - creating."
|
|
aws --profile "${PROFILE}" iam create-role \
|
|
--role-name "${ROLE_NAME}" \
|
|
--assume-role-policy-document "${TRUST_POLICY}" \
|
|
--description "TEMPORARY read-only DynamoDB access for Luby initial assessment from seahaven-external-dev; trust self-expires 2026-08-23" \
|
|
--max-session-duration 3600 \
|
|
--tags Key=project,Value=procurement-ingest Key=managed-by,Value=create-assessment-reader.sh Key=temporary,Value=expires-2026-08-23
|
|
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}"
|
|
|
|
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. Assessment reader ready (expires 2026-08-23T00:00:00Z):"
|
|
echo " ${ROLE_ARN}"
|