#!/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