Admit and install the backup pair writer.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

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:
codex 2026-10-08 22:41:20 +02:00
parent acbc78ffea
commit 9b658e3f19
8 changed files with 280 additions and 8 deletions

View file

@ -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",

View 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

View 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

View 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