Files
acdl/terraform/bootstrap/create_iam_user.py
T
Jon Chery f8ddd8b182 phase: 8, status: plan-as-execute, persona: security-engineer+platform-engineer, task: T-8.1..T-8.4
---ci---
project: acdl
phase: 8
milestone: v1.1
status: plan-as-execute
persona: security-engineer+platform-engineer
task: [T-8.1, T-8.2, T-8.3, T-8.4]
requirements.covered: [REQ-23]
---/ci---

Waves 1+2: IAM policy + state backend + IAM user creation scripts.

- T-8.1 (security): terraform/bootstrap/spike_runner_policy.json —
  least-privilege IAM policy: Allow S3 r/w on the state bucket, DynamoDB
  r/w on the outbox table, sts:GetCallerIdentity; final Deny statement
  (Action *, NotResource = the above ARNs) enforcing least privilege. No
  terraform apply permission (plan-only spike).

- T-8.2/T-8.3 (platform): terraform/bootstrap/create_state_backend.py —
  boto3, idempotent: creates S3 bucket acdl-tfstate-581513795199-us-east-1
  (versioning enabled) + DynamoDB table acdl-outbox (PAY_PER_REQUEST, PK
  contractId, SK eventType#eventTs per D-P08-1 one table for both lock
  + outbox). Writes .bootstrap_state.json marker.

- T-8.4 (platform + security review): terraform/bootstrap/create_iam_user.py
  — boto3, idempotent: creates IAM user acdl-spike-runner, attaches the
  inline policy from spike_runner_policy.json, creates an initial access
  key if none active exists (prints to stdout for the orchestrator to
  capture; NEVER committed).

py_compile + policy JSON valid.
2026-07-21 18:58:29 +00:00

72 lines
2.6 KiB
Python

"""Create the ACDL v1.1 spike IAM user + scoped inline policy + initial key.
Idempotent: skips user creation if the user exists; creates an initial
access key if none active exists. Prints the key to stdout for the
orchestrator to capture (NEVER committed):
ACDL_AWS_ACCESS_KEY_ID=<...>
ACDL_AWS_SECRET_ACCESS_KEY=<...>
Run with the bootstrap root key in env:
ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID / ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY
AWS_DEFAULT_REGION (defaults to us-east-1)
The inline policy is read from spike_runner_policy.json (next to this
file). The account id + region are already substituted in the policy file
for account 581513795199 + us-east-1; this script does not substitute
further (the policy file is spike-specific).
"""
import json
import os
import sys
import boto3
REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
USER_NAME = "acdl-spike-runner"
POLICY_NAME = "acdl-spike-runner-policy"
POLICY_FILE = os.path.join(os.path.dirname(__file__), "spike_runner_policy.json")
def main():
session = boto3.Session(
aws_access_key_id=os.environ["ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID"],
aws_secret_access_key=os.environ["ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY"],
region_name=REGION,
)
iam = session.client("iam")
# --- IAM user (idempotent) ---
try:
iam.get_user(UserName=USER_NAME)
print(f"iam: user {USER_NAME} already exists")
except iam.exceptions.NoSuchEntityException:
iam.create_user(UserName=USER_NAME)
print(f"iam: created user {USER_NAME}")
# --- Inline policy (idempotent: put_user_policy overwrites) ---
with open(POLICY_FILE, "r") as fh:
policy_doc = fh.read()
iam.put_user_policy(
UserName=USER_NAME,
PolicyName=POLICY_NAME,
PolicyDocument=policy_doc,
)
print(f"iam: inline policy {POLICY_NAME} attached to {USER_NAME}")
# --- Initial access key (create only if no active key exists) ---
keys = iam.list_access_keys(UserName=USER_NAME).get("AccessKeyMetadata", [])
active = [k for k in keys if k["Status"] == "Active"]
if active:
print(f"iam: {USER_NAME} already has {len(active)} active key(s); not creating a new one")
print(" (use scripts/rotate_spike_key.sh to rotate)")
return
new_key = iam.create_access_key(UserName=USER_NAME)["AccessKey"]
print("ACDL_AWS_ACCESS_KEY_ID=" + new_key["AccessKeyId"])
print("ACDL_AWS_SECRET_ACCESS_KEY=" + new_key["SecretAccessKey"])
print(f"iam: created initial access key {new_key['AccessKeyId']} for {USER_NAME}", file=sys.stderr)
if __name__ == "__main__":
main()