feat: apply prefix-scoped backup bucket policy when APPLICATION_ID is in bao
This commit is contained in:
parent
7965a7295f
commit
9ee3a265bb
1 changed files with 165 additions and 0 deletions
165
tools/apply-backup-bucket-policy.sh
Executable file
165
tools/apply-backup-bucket-policy.sh
Executable 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue