#!/usr/bin/env bash # scripts/rotate_spike_key.sh - rotate the acdl-spike-runner IAM access key. # # Uses the bootstrap root key (ACDL_BOOTSTRAP_AWS_*) from the env to: # 1. List acdl-spike-runner's access keys. # 2. Create a new key. # 3. Deactivate + delete the old key(s). # 4. Write the new key to gitignored .env.secrets (chmod 600). # 5. Optionally upload to Gitea secrets if ACDL_GITEA_TOKEN is set. # # Idempotent: re-running always ends with exactly 1 active key for the user. # Does NOT rotate the bootstrap root key (D-034 closure = manual user step). # # Spike scope (D-039): the spike user key is per-run-rotated; real OIDC is # v1.2 (blocked on go-gitea/gitea#36988). set -u ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" ENV_FILE="$ROOT/.env.secrets" fail() { echo "FAIL: $*" >&2; exit 1; } : "${ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID:?set ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID to the root key}" : "${ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY:?set ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY to the root key}" REGION="${AWS_DEFAULT_REGION:-us-east-1}" USER_NAME="acdl-spike-runner" # Confirm .env.secrets is gitignored before writing to it. git check-ignore -q "$ENV_FILE" || fail "$ENV_FILE is not gitignored — refusing to write the key" python3 - <<'PY' import os import sys import json import boto3 region = os.environ.get("AWS_DEFAULT_REGION", "us-east-1") user = "acdl-spike-runner" env_file = os.path.join(os.getcwd(), ".env.secrets") 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") # List current keys. keys = iam.list_access_keys(UserName=user).get("AccessKeyMetadata", []) active = [k for k in keys if k["Status"] == "Active"] # Create a new key first (so the user always has a working key during rotation). new = iam.create_access_key(UserName=user)["AccessKey"] new_id = new["AccessKeyId"] new_secret = new["SecretAccessKey"] print(f"iam: created new key {new_id} for {user}", file=sys.stderr) # Deactivate + delete the old keys. for k in active: old_id = k["AccessKeyId"] if old_id == new_id: continue iam.update_access_key(UserName=user, AccessKeyId=old_id, Status="Inactive") iam.delete_access_key(UserName=user, AccessKeyId=old_id) print(f"iam: deactivated+deleted old key {old_id}", file=sys.stderr) # Write the new key to gitignored .env.secrets (chmod 600). with open(env_file, "w") as fh: fh.write(f"ACDL_AWS_ACCESS_KEY_ID={new_id}\n") fh.write(f"ACDL_AWS_SECRET_ACCESS_KEY={new_secret}\n") fh.write(f"AWS_DEFAULT_REGION={region}\n") os.chmod(env_file, 0o600) print(f"rotated key written to {env_file} (chmod 600)", file=sys.stderr) # Optionally upload to Gitea secrets. gitea_token = os.environ.get("ACDL_GITEA_TOKEN") if gitea_token: import urllib.request base = "https://git.cloudinit.dev/api/v1/repos/continuous-intelligence/acdl/actions/secrets" for name, value in [("ACDL_AWS_ACCESS_KEY_ID", new_id), ("ACDL_AWS_SECRET_ACCESS_KEY", new_secret)]: req = urllib.request.Request( f"{base}/{name}", data=json.dumps({"value": value}).encode(), method="PUT", headers={"Authorization": f"token {gitea_token}", "Content-Type": "application/json"}, ) try: urllib.request.urlopen(req).read() print(f"gitea: secret {name} uploaded", file=sys.stderr) except Exception as e: print(f"gitea: secret {name} upload FAILED: {e}", file=sys.stderr) else: print("gitea: ACDL_GITEA_TOKEN not set; Gitea secret upload skipped (v1.2 hardening)", file=sys.stderr) print(f"OK: {user} now has exactly 1 active key: {new_id}") PY