feat: apply prefix-scoped backup bucket policy when APPLICATION_ID is in bao

This commit is contained in:
tegwick 2026-08-14 19:52:06 +02:00
parent 7965a7295f
commit 9ee3a265bb

View file

@ -0,0 +1,165 @@
#!/usr/bin/env bash
# Apply a prefix-scoped bucket policy for the resource-control backup key.
# Reads APPLICATION_ID from OpenBao. Never prints secret values.
# Uses the bootstrap key to PutBucketPolicy (org owner can always edit policies).
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
BAO_ADDR="${BAO_ADDR:-${VAULT_ADDR:-https://bao.coulomb.social}}"
REGION="nl-ams"
BUCKET="${BUCKET:-railiance-platform-pg-backup}"
PREFIX="${PREFIX:-platform-pg/}"
need() { command -v "$1" >/dev/null || { echo "missing $1" >&2; exit 2; }; }
export PATH="${HOME}/.local/bin:${PATH}"
need python3
need curl
TOKEN="${OPENBAO_TOKEN:-${VAULT_TOKEN:-}}"
if [[ -z "$TOKEN" && -f "$HOME/.vault-token" ]]; then
TOKEN="$(cat "$HOME/.vault-token")"
fi
[[ -n "$TOKEN" ]] || { echo "no OpenBao token" >&2; exit 2; }
read_kv() {
local path="$1"
curl -fsS -H "X-Vault-Token: $TOKEN" \
"$BAO_ADDR/v1/platform/data/${path#platform/}" \
| python3 -c 'import json,sys; d=json.load(sys.stdin); print(json.dumps(d["data"]["data"]))'
}
BACKUP_JSON="$(read_kv workloads/railiance/backup/object-storage)"
BOOTSTRAP_JSON="$(read_kv workloads/railiance/scaleway/bootstrap)"
EXPORTS="$(
BACKUP_JSON="$BACKUP_JSON" BOOTSTRAP_JSON="$BOOTSTRAP_JSON" python3 - <<'PY'
import json, os, sys, re
backup = json.loads(os.environ["BACKUP_JSON"])
boot = json.loads(os.environ["BOOTSTRAP_JSON"])
app = (backup.get("APPLICATION_ID") or "").strip()
if not app or app in {"xxx", "redacted", "changeme"}:
sys.stderr.write(
"APPLICATION_ID missing on platform/workloads/railiance/backup/object-storage\n"
"Founder: add that field in the OpenBao UI (IAM application resource-control UUID).\n"
"It is not a secret. Then re-run this script.\n"
)
sys.exit(6)
if not re.fullmatch(r"[0-9a-fA-F-]{36}", app):
sys.stderr.write("APPLICATION_ID is not a 36-char UUID\n")
sys.exit(7)
def g(data, *ks):
for k in ks:
if data.get(k):
return data[k]
return ""
print("export APPLICATION_ID=" + json.dumps(app))
print("export SCW_ACCESS_KEY=" + json.dumps(g(boot, "ACCESS_KEY")))
print("export SCW_SECRET_KEY=" + json.dumps(g(boot, "SECRET_KEY")))
print("export BACKUP_ACCESS_KEY=" + json.dumps(g(backup, "ACCESS_KEY", "ACCESS_KEY_ID")))
print("export BACKUP_SECRET_KEY=" + json.dumps(g(backup, "SECRET_KEY", "ACCESS_SECRET_KEY")))
PY
)"
eval "$EXPORTS"
export SCW_DEFAULT_REGION="$REGION"
export AWS_ACCESS_KEY_ID="$SCW_ACCESS_KEY"
export AWS_SECRET_ACCESS_KEY="$SCW_SECRET_KEY"
VENV="${TMPDIR:-/tmp}/reef-storage-boto3"
if [[ ! -x "$VENV/bin/python" ]]; then
python3 -m venv "$VENV"
"$VENV/bin/pip" -q install boto3
fi
echo "putting prefix-scoped bucket policy on $BUCKET (application id not printed)"
BUCKET="$BUCKET" PREFIX="$PREFIX" APPLICATION_ID="$APPLICATION_ID" \
"$VENV/bin/python" - <<'PY'
import json, os
import boto3
bucket = os.environ["BUCKET"]
prefix = os.environ["PREFIX"]
app = os.environ["APPLICATION_ID"]
policy = {
"Version": "2023-04-17",
"Id": "resource-control-scoped-backup",
"Statement": [
{
"Sid": "BackupListPrefix",
"Effect": "Allow",
"Principal": {"SCW": f"application_id:{app}"},
"Action": [
"s3:ListBucket",
"s3:ListBucketMultipartUploads",
"s3:ListBucketVersions",
"s3:GetBucketLocation",
"s3:GetBucketVersioning",
"s3:GetLifecycleConfiguration",
],
"Resource": [bucket],
"Condition": {"StringLike": {"s3:prefix": [prefix, prefix + "*"]}},
},
{
"Sid": "BackupObjectsUnderPrefix",
"Effect": "Allow",
"Principal": {"SCW": f"application_id:{app}"},
"Action": [
"s3:GetObject",
"s3:GetObjectVersion",
"s3:PutObject",
"s3:DeleteObject",
"s3:DeleteObjectVersion",
"s3:AbortMultipartUpload",
"s3:ListMultipartUploadParts",
],
"Resource": [f"{bucket}/{prefix}*"],
},
],
}
client = boto3.client(
"s3",
region_name="nl-ams",
endpoint_url="https://s3.nl-ams.scw.cloud",
aws_access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
aws_secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"],
)
client.put_bucket_policy(Bucket=bucket, Policy=json.dumps(policy))
print("bucket_policy=put")
got = client.get_bucket_policy(Bucket=bucket)
body = json.loads(got["Policy"])
print("statements", [s.get("Sid") for s in body.get("Statement", [])])
PY
echo "verifying backup key: prefix ok, sibling denied"
BUCKET="$BUCKET" PREFIX="$PREFIX" \
AWS_ACCESS_KEY_ID="$BACKUP_ACCESS_KEY" AWS_SECRET_ACCESS_KEY="$BACKUP_SECRET_KEY" \
"$VENV/bin/python" - <<'PY'
import os
from botocore.exceptions import ClientError
import boto3
c = boto3.client(
"s3",
region_name="nl-ams",
endpoint_url="https://s3.nl-ams.scw.cloud",
aws_access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
aws_secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"],
)
bucket = os.environ["BUCKET"]
prefix = os.environ["PREFIX"]
ok = prefix + ".policy-probe"
sib = "other-consumer/.policy-probe"
try:
c.put_object(Bucket=bucket, Key=ok, Body=b"ok")
c.delete_object(Bucket=bucket, Key=ok)
print("prefix_write=ok")
except ClientError as e:
print("prefix_write=fail", e.response.get("Error", {}).get("Code"))
try:
c.put_object(Bucket=bucket, Key=sib, Body=b"no")
print("sibling_write=UNEXPECTED_OK")
c.delete_object(Bucket=bucket, Key=sib)
except ClientError as e:
print("sibling_write=denied", e.response.get("Error", {}).get("Code"))
PY
echo "done"
unset SCW_SECRET_KEY SCW_ACCESS_KEY BACKUP_SECRET_KEY BACKUP_ACCESS_KEY AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY APPLICATION_ID