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
153 lines
4.9 KiB
Python
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
|