Docs + tooling: README phase-2 backup, iam-user-delete script (#10)

README: document AWS Backup phase-2 (phase2-offsite-everything selection),
remove retired database-1 from phase-1 scope (audit H-19), update roadmap +
verify smoke-test to a live resource.

scripts/iam-user-delete.sh: reusable full IAM user teardown (keys, policies,
groups, MFA, login profile, certs, SSH keys, service creds, then user) with
--profile/--yes and a guard against deleting the caller's own identity. Built
from the Day 4 audit IAM cleanup.
This commit is contained in:
Adam Moussa 2026-06-03 13:15:18 -04:00 • committed by GitHub
parent 1d6668090c
commit 0dd8d2a7af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 148 additions and 4 deletions

View file

@ -59,9 +59,12 @@ recovery point cross-region into a governance-locked vault.
| Service role | `seahaven-backup-service-role` | **Backup-only** (Backup + S3-Backup managed policies); restore perms intentionally deferred |
**Phase-1 scope** (selected by explicit ARN, not tags, to avoid drifting other
stacks): RDS `database-1`, RDS `proposal-system-db`, DynamoDB `PaymentsDashboard`,
stacks): RDS `proposal-system-db`, DynamoDB `PaymentsDashboard`,
DynamoDB `purchase-orders`, S3 `accounting.seahaven.com`,
`seahaven-payments-csv-328440206208`, `google-workspace-seahavenind.com`.
*(RDS `database-1` was originally in this set but was retired 2026-06-03 —
audit H-19, idle 0 conn/60d — and removed from the selection; its final
encrypted recovery point is retained in `seahaven-offsite` for 7 years.)*
**Coexists with** existing EBS DLM snapshots and DynamoDB PITR — it supplements
them with the missing offsite + immutable leg; it does not replace them.
@ -84,6 +87,35 @@ them with the missing offsite + immutable leg; it does not replace them.
it to us-west-2 first; if the copy fails, encrypt it or drop it from the copy.
3. Enable DynamoDB PITR (H-7) on the two tables for between-window recovery.
### AWS Backup phase 2 (audit Day 4)
Expands the same `seahaven-critical-daily` plan to every remaining data store, so
all of DynamoDB + EBS get the offsite + immutable leg ("offsite for everything").
| Resource | Logical ID | Notes |
|---|---|---|
| Phase-2 selection | `Plan/Phase2Resources` (`phase2-offsite-everything`) | Same plan, same `seahaven-backup-service-role`, same daily + cross-region copy rule |
**Phase-2 scope:** the 15 remaining DynamoDB tables (all except the two phase-1
financial tables + the deleted ledgerflow tables) and all 9 in-use EBS volumes,
again **by explicit ARN** — tag-based selection was deliberately avoided because
the file-share volumes are standalone-managed and the tables are owned by other
stacks, so tagging here would drift them.
**No IAM change:** `AWSBackupServiceRolePolicyForBackup` already grants the
DynamoDB/RDS/EBS backup actions, so phase 2 reuses the phase-1 role unchanged
(cross-reviewed, no BLOCK).
**Known tradeoff (→ Jira INFRA-31):** explicit-ARN EBS entries go stale if a
volume is replaced (new volume id), silently dropping it from backup. Migrating
the EBS portion to tag-based selection (with the tag codified in each owning
stack) is the resilient follow-up; scheduled drift detection is the interim
backstop.
**Also enabled outside this stack (audit H-7, via CLI — codify per stack →
INFRA-30):** PITR + `DeletionProtectionEnabled` on 12 more DynamoDB tables
(account-wide PITR now 19/21).
### Detective controls + budget (audit Day 1)
Account-level detective layer, in `lib/detective-controls.ts`, plus the cost
@ -184,8 +216,9 @@ aws cloudwatch put-metric-alarm --alarm-name forgejo-backup-verification-errors
## Roadmap (same stack)
Detective layer multi-region expansion (GuardDuty/Config/Security Hub beyond
us-east-1). Backup phase 2: expand past the phase-1 set via tag-based selection
and graduate the offsite vault to compliance mode.
us-east-1). Backup: phase 2 is deployed (see above); remaining is migrating the
phase-2 EBS entries to tag-based selection (INFRA-31) and graduating the offsite
vault to compliance mode.
## Deploy
@ -214,9 +247,11 @@ aws backup describe-backup-vault --backup-vault-name seahaven-offsite --region u
aws backup get-backup-plan --backup-plan-id <id> # daily rule + CopyAction
# Smoke test: on-demand backup of one resource, then confirm the cross-region copy lands
aws backup start-backup-job --backup-vault-name seahaven-primary \
--resource-arn arn:aws:rds:us-east-1:328440206208:db:database-1 \
--resource-arn arn:aws:rds:us-east-1:328440206208:db:proposal-system-db \
--iam-role-arn arn:aws:iam::328440206208:role/seahaven-backup-service-role
aws backup list-copy-jobs --region us-west-2 # copy to offsite present + COMPLETED
# Phase-2 selections live on the plan:
aws backup list-backup-selections --backup-plan-id <id> --query 'BackupSelectionsList[].SelectionName' # critical-data + phase2-offsite-everything
```
Detective layer + governance (Day 1):

109
scripts/iam-user-delete.sh Executable file
View file

@ -0,0 +1,109 @@
#!/usr/bin/env bash
#
# iam-user-delete.sh — fully delete one or more IAM users (account 328440206208).
#
# IAM refuses to delete a user that still has attached/inline policies, access
# keys, group memberships, MFA devices, a login profile, signing certs, SSH keys,
# or service-specific credentials. This tears all of that down in order, then
# deletes the user. Built from the Day 4 audit cleanup (frappe/termius/ledgerflow,
# then office_mac/home_desktop/Personal-laptop). See reference memory
# `reference_identity_center_workmail` for the SSO context.
#
# Usage:
# scripts/iam-user-delete.sh [--profile NAME] [--yes] USER [USER ...]
#
# --profile NAME AWS CLI profile (default: $AWS_PROFILE or the default chain).
# Post-SSO-cutover this is normally `amoussa-seahaven`.
# --yes Skip the per-user confirmation prompt.
#
# Safety:
# * Refuses to delete the user the current credentials authenticate as.
# * Deactivates access keys before deleting them (a brief, reversible window
# if you remove --yes and inspect between users).
# * Prints each user's attachments before deleting so there is a record.
#
set -euo pipefail
PROFILE_ARG=()
ASSUME_YES=0
USERS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--profile) PROFILE_ARG=(--profile "$2"); shift 2 ;;
--yes|-y) ASSUME_YES=1; shift ;;
-h|--help) sed -n '2,30p' "$0"; exit 0 ;;
-*) echo "unknown flag: $1" >&2; exit 2 ;;
*) USERS+=("$1"); shift ;;
esac
done
[[ ${#USERS[@]} -eq 0 ]] && { echo "usage: $0 [--profile NAME] [--yes] USER [USER ...]" >&2; exit 2; }
aws_() { aws "${PROFILE_ARG[@]}" "$@"; }
# Guard: never delete the identity we're running as.
SELF_ARN="$(aws_ sts get-caller-identity --query Arn --output text)"
echo "running as: $SELF_ARN"
delete_user() {
local u="$1"
if ! aws_ iam get-user --user-name "$u" >/dev/null 2>&1; then
echo " $u: does not exist, skipping"; return 0
fi
if [[ "$SELF_ARN" == *":user/$u" ]]; then
echo " $u: REFUSING — this is the identity you are authenticated as" >&2; return 1
fi
echo "== $u =="
echo " keys: $(aws_ iam list-access-keys --user-name "$u" --query 'AccessKeyMetadata[].AccessKeyId' --output text)"
echo " attached: $(aws_ iam list-attached-user-policies --user-name "$u" --query 'AttachedPolicies[].PolicyName' --output text)"
echo " inline: $(aws_ iam list-user-policies --user-name "$u" --query 'PolicyNames' --output text)"
echo " groups: $(aws_ iam list-groups-for-user --user-name "$u" --query 'Groups[].GroupName' --output text)"
if [[ $ASSUME_YES -eq 0 ]]; then
read -r -p " delete user '$u'? [y/N] " ans
[[ "$ans" =~ ^[Yy]$ ]] || { echo " skipped"; return 0; }
fi
# access keys: deactivate (reversible) then delete
for k in $(aws_ iam list-access-keys --user-name "$u" --query 'AccessKeyMetadata[].AccessKeyId' --output text); do
aws_ iam update-access-key --user-name "$u" --access-key-id "$k" --status Inactive
aws_ iam delete-access-key --user-name "$u" --access-key-id "$k"
done
# detach managed policies
for p in $(aws_ iam list-attached-user-policies --user-name "$u" --query 'AttachedPolicies[].PolicyArn' --output text); do
aws_ iam detach-user-policy --user-name "$u" --policy-arn "$p"
done
# delete inline policies
for ip in $(aws_ iam list-user-policies --user-name "$u" --query 'PolicyNames[]' --output text); do
aws_ iam delete-user-policy --user-name "$u" --policy-name "$ip"
done
# remove from groups
for g in $(aws_ iam list-groups-for-user --user-name "$u" --query 'Groups[].GroupName' --output text); do
aws_ iam remove-user-from-group --user-name "$u" --group-name "$g"
done
# MFA devices
for m in $(aws_ iam list-mfa-devices --user-name "$u" --query 'MFADevices[].SerialNumber' --output text); do
aws_ iam deactivate-mfa-device --user-name "$u" --serial-number "$m"
done
# login profile (console password)
aws_ iam delete-login-profile --user-name "$u" 2>/dev/null || true
# signing certs
for c in $(aws_ iam list-signing-certificates --user-name "$u" --query 'Certificates[].CertificateId' --output text 2>/dev/null); do
aws_ iam delete-signing-certificate --user-name "$u" --certificate-id "$c"
done
# SSH public keys (CodeCommit)
for s in $(aws_ iam list-ssh-public-keys --user-name "$u" --query 'SSHPublicKeys[].SSHPublicKeyId' --output text 2>/dev/null); do
aws_ iam delete-ssh-public-key --user-name "$u" --ssh-public-key-id "$s"
done
# service-specific credentials
for sc in $(aws_ iam list-service-specific-credentials --user-name "$u" --query 'ServiceSpecificCredentials[].ServiceSpecificCredentialId' --output text 2>/dev/null); do
aws_ iam delete-service-specific-credential --user-name "$u" --service-specific-credential-id "$sc"
done
aws_ iam delete-user --user-name "$u"
echo " $u: deleted"
}
rc=0
for u in "${USERS[@]}"; do delete_user "$u" || rc=1; done
exit $rc