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
This commit is contained in:
parent
acbc78ffea
commit
9b658e3f19
8 changed files with 280 additions and 8 deletions
|
|
@ -18,7 +18,9 @@ from pathlib import Path
|
|||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FIELDS = ("AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY")
|
||||
WRITER_POLICY = "backup-pair-writer"
|
||||
WRITER_ADMITTED = False
|
||||
# Founder admission 2026-10-08. The installed OpenBao policy matches
|
||||
# openbao/policies/backup-pair-writer.hcl. The CLI still does not write.
|
||||
WRITER_ADMITTED = True
|
||||
PROFILES = {
|
||||
"coulomb-social": "CCR-2026-0036",
|
||||
"email-connect": "CCR-2026-0037",
|
||||
|
|
|
|||
11
scripts/install-backup-pair-writer.sh
Executable file
11
scripts/install-backup-pair-writer.sh
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
#!/bin/sh
|
||||
# Install backup-pair-writer. Silent: warden access rejects any child output.
|
||||
set -eu
|
||||
policy=/home/worsch/railiance-platform/openbao/policies/backup-pair-writer.hcl
|
||||
receipt_dir=/home/worsch/.local/state/railiance/receipts
|
||||
umask 077
|
||||
mkdir -p "$receipt_dir"
|
||||
bao policy write backup-pair-writer "$policy" >/dev/null 2>&1 || exit 31
|
||||
bao policy read backup-pair-writer >"$receipt_dir/backup-pair-writer.policy" 2>/dev/null || exit 32
|
||||
sha256sum "$receipt_dir/backup-pair-writer.policy" >"$receipt_dir/backup-pair-writer.sha256" || exit 33
|
||||
exit 0
|
||||
153
scripts/survey-backup-iam.py
Normal file
153
scripts/survey-backup-iam.py
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
#!/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
|
||||
36
scripts/survey-backup-pair-metadata.sh
Executable file
36
scripts/survey-backup-pair-metadata.sh
Executable file
|
|
@ -0,0 +1,36 @@
|
|||
#!/bin/sh
|
||||
# Record KV metadata versions only. Silent for warden access.
|
||||
set -eu
|
||||
receipt_dir=/home/worsch/.local/state/railiance/receipts
|
||||
umask 077
|
||||
mkdir -p "$receipt_dir"
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
python3 - "$tmp" "$receipt_dir/backup-pair-metadata.json" << 'PY'
|
||||
import json, subprocess, sys
|
||||
tmp, dest = sys.argv[1:]
|
||||
paths = [
|
||||
"platform/workloads/coulomb-social/backup-primary",
|
||||
"platform/workloads/email-connect/backup-primary",
|
||||
"platform/workloads/railiance/scaleway/bootstrap",
|
||||
"platform/workloads/railiance/backup/object-storage",
|
||||
]
|
||||
rows = []
|
||||
for path in paths:
|
||||
out = subprocess.run(
|
||||
["bao", "kv", "metadata", "get", "-format=json", path],
|
||||
capture_output=True,
|
||||
)
|
||||
row = {"path": path, "returncode": out.returncode}
|
||||
if out.returncode == 0:
|
||||
data = json.loads(out.stdout)["data"]
|
||||
row["current_version"] = data.get("current_version")
|
||||
versions = data.get("versions") or {}
|
||||
row["version_count"] = len(versions)
|
||||
else:
|
||||
err = (out.stderr + out.stdout).decode("utf-8", "replace")
|
||||
row["absent"] = "No value found" in err
|
||||
rows.append(row)
|
||||
open(dest, "w").write(json.dumps({"paths": rows}, indent=2) + "\n")
|
||||
PY
|
||||
exit 0
|
||||
Loading…
Add table
Add a link
Reference in a new issue