165 lines
5.7 KiB
Bash
Executable file
165 lines
5.7 KiB
Bash
Executable file
#!/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
|