railiance-platform/scripts/survey-backup-iam.py
codex 9b658e3f19
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Admit and install the backup pair writer.
The OpenBao policy matches the reviewed file. Both publisher KV paths are still empty because the bootstrap key cannot create IAM applications.

Assistant: grok
Assistant-Session: 01a11cdb-f9fe-7011-947c-936e72007e54
2026-10-08 22:41:20 +02:00

153 lines
4.9 KiB
Python

#!/usr/bin/env python3
"""Survey Scaleway IAM names and the backup bucket policy.
Reads the bootstrap and scoped-backup pairs in memory. Writes names, ids and
the bucket policy only. Never prints a credential.
"""
from __future__ import annotations
import datetime as dt
import hashlib
import hmac
import json
import subprocess
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
RECEIPT = Path("/home/worsch/.local/state/railiance/receipts/backup-iam-survey.json")
STAGE = Path("/home/worsch/.local/state/railiance/receipts/backup-iam-survey.stage")
BUCKET = "railiance-platform-pg-backup"
HOST = "s3.nl-ams.scw.cloud"
def stage(text):
STAGE.parent.mkdir(mode=0o700, exist_ok=True)
STAGE.write_text(text + "\n")
STAGE.chmod(0o600)
def kv(path):
proc = subprocess.run(
["bao", "kv", "get", "-format=json", path],
capture_output=True,
)
if proc.returncode:
stage(f"kv_read_failed {path} {proc.returncode}")
raise SystemExit(41)
stage(f"kv_read_ok {path}")
return json.loads(proc.stdout)["data"]["data"]
def scaleway(secret, url):
request = urllib.request.Request(url, headers={"X-Auth-Token": secret})
try:
with urllib.request.urlopen(request, timeout=30) as response:
return response.status, json.loads(response.read().decode())
except urllib.error.HTTPError as exc:
return exc.code, None
def sign(access_key, secret_key, canonical_uri, query):
now = dt.datetime.now(dt.timezone.utc)
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
day = now.strftime("%Y%m%d")
payload = hashlib.sha256(b"").hexdigest()
canonical_query = urllib.parse.urlencode(sorted(query.items()))
canonical = "\n".join(
[
"GET",
canonical_uri,
canonical_query,
f"host:{HOST}",
f"x-amz-content-sha256:{payload}",
f"x-amz-date:{amz_date}",
"",
"host;x-amz-content-sha256;x-amz-date",
payload,
]
)
scope = f"{day}/nl-ams/s3/aws4_request"
to_sign = "\n".join(
["AWS4-HMAC-SHA256", amz_date, scope, hashlib.sha256(canonical.encode()).hexdigest()]
)
key = ("AWS4" + secret_key).encode()
for part in (day, "nl-ams", "s3", "aws4_request"):
key = hmac.new(key, part.encode(), hashlib.sha256).digest()
signature = hmac.new(key, to_sign.encode(), hashlib.sha256).hexdigest()
return {
"Authorization": (
"AWS4-HMAC-SHA256 "
f"Credential={access_key}/{scope}, "
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, "
f"Signature={signature}"
),
"x-amz-date": amz_date,
"x-amz-content-sha256": payload,
}
def bucket_policy(access_key, secret_key):
query = {"policy": ""}
headers = sign(access_key, secret_key, f"/{BUCKET}", query)
url = f"https://{HOST}/{BUCKET}?policy"
request = urllib.request.Request(url, headers=headers)
try:
with urllib.request.urlopen(request, timeout=30) as response:
return response.status, json.loads(response.read().decode())
except urllib.error.HTTPError as exc:
return exc.code, None
def main():
boot = kv("platform/workloads/railiance/scaleway/bootstrap")
backup = kv("platform/workloads/railiance/backup/object-storage")
org = boot["DEFAULT_ORGANIZATION_ID"]
project = boot["DEFAULT_PROJECT_ID"]
app_status, applications = scaleway(
boot["SECRET_KEY"],
"https://api.scaleway.com/iam/v1alpha1/applications"
f"?organization_id={urllib.parse.quote(org)}&page_size=100",
)
policy_status, policies = scaleway(
boot["SECRET_KEY"],
"https://api.scaleway.com/iam/v1alpha1/policies"
f"?organization_id={urllib.parse.quote(org)}&page_size=100",
)
s3_status, document = bucket_policy(backup["ACCESS_KEY"], backup["SECRET_KEY"])
receipt = {
"organization_id": org,
"project_id": project,
"application_list_status": app_status,
"applications": [
{"id": item.get("id"), "name": item.get("name")}
for item in (applications or {}).get("applications", [])
],
"policy_list_status": policy_status,
"policies": [
{
"id": item.get("id"),
"name": item.get("name"),
"application_id": (item.get("application_id") or item.get("principal", {}).get("id")),
}
for item in (policies or {}).get("policies", [])
],
"bucket_policy_status": s3_status,
"bucket_policy": document,
}
RECEIPT.parent.mkdir(mode=0o700, exist_ok=True)
RECEIPT.write_text(json.dumps(receipt, indent=2) + "\n")
RECEIPT.chmod(0o600)
if __name__ == "__main__":
try:
main()
stage("ok")
except SystemExit:
raise
except Exception as exc:
stage("failed " + type(exc).__name__)
raise SystemExit(42) from None