Files
familienarchiv/infra/minio/bootstrap.sh
Marcel 91f70e652d security(minio): scope archiv-app to bucket-only IAM policy
Replaces MinIO's built-in `readwrite` policy (which grants s3:* on
arn:aws:s3:::* — every bucket present and future) with a bucket-scoped
custom policy `archiv-app-policy`:

  - s3:GetObject / s3:PutObject / s3:DeleteObject on familienarchiv/*
  - s3:ListBucket / s3:GetBucketLocation on familienarchiv

The previous configuration silently regressed the least-privilege guarantee
that the service-account separation was supposed to provide: a future
second bucket (logs, backups, mc-mirror staging) would have been
read/write/delete-accessible to a compromised backend.

While at it, two follow-on fixes:

  1. Extract the entrypoint to infra/minio/bootstrap.sh. The previous
     inline `/bin/sh -c "..."` was already at the YAML-escaping ceiling;
     adding the policy-JSON heredoc would have made it unreadable.

  2. Replace the `| grep -q readwrite || exit 1` fatal-check with a
     POSIX `case` substring match. The minio/mc image ships coreutils +
     bash but NOT grep/awk/sed — the original check was a no-op that
     ALWAYS exited 1 (verified locally). The new check passes on the
     first invocation and on every subsequent re-deploy.

Idempotency verified locally: two consecutive `docker compose run --rm
create-buckets` invocations both exit 0 with the user bound to the
new policy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 13:07:56 +02:00

68 lines
2.2 KiB
Bash
Executable File

#!/bin/sh
# Idempotent MinIO bootstrap for the Familienarchiv stack.
#
# Runs on every `docker compose up` (the create-buckets service is one-shot,
# no restart). Each step swallows the "already exists" error so the script
# is safe to re-run.
#
# What it does:
# 1. Register the MinIO alias using the root credentials
# 2. Create the application bucket if missing
# 3. Lock the bucket to private (defense in depth)
# 4. Create/enable the `archiv-app` service account (least-privilege user)
# 5. Install a bucket-scoped policy `archiv-app-policy`:
# - GetObject/PutObject/DeleteObject on familienarchiv/*
# - ListBucket + GetBucketLocation on familienarchiv
# (Replaces MinIO's built-in `readwrite` which grants s3:* on *.)
# 6. Attach the policy to `archiv-app`
# 7. Fatal assertion: read back the user and confirm the policy is bound.
# Uses `case` (POSIX) for substring match — the minio/mc image ships
# coreutils + bash but NOT grep/awk/sed.
#
# Required env vars: MINIO_PASSWORD, MINIO_APP_PASSWORD
set -e
mc alias set myminio http://minio:9000 archiv "$MINIO_PASSWORD"
mc mb myminio/familienarchiv --ignore-existing
mc anonymous set private myminio/familienarchiv
mc admin user add myminio archiv-app "$MINIO_APP_PASSWORD" \
|| mc admin user enable myminio archiv-app
cat > /tmp/archiv-app-policy.json <<'POLICY'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": ["arn:aws:s3:::familienarchiv/*"]
},
{
"Effect": "Allow",
"Action": ["s3:ListBucket", "s3:GetBucketLocation"],
"Resource": ["arn:aws:s3:::familienarchiv"]
}
]
}
POLICY
mc admin policy create myminio archiv-app-policy /tmp/archiv-app-policy.json 2>/dev/null \
|| mc admin policy update myminio archiv-app-policy /tmp/archiv-app-policy.json
mc admin policy attach myminio archiv-app-policy --user archiv-app 2>/dev/null || true
INFO=$(mc admin user info myminio archiv-app)
case "$INFO" in
*archiv-app-policy*)
echo "archiv-app bound to archiv-app-policy"
;;
*)
echo "FATAL: archiv-app is missing the bucket-scoped policy"
echo "----- user info -----"
echo "$INFO"
exit 1
;;
esac