security: restrict MinIO app to one bucket

This commit is contained in:
ik
2026-09-12 16:01:00 +07:00
parent 926182a6cf
commit 868278fdba
10 changed files with 23 additions and 15 deletions
+2
View File
@@ -42,6 +42,8 @@ RF4DB/RF4-STAT/RF4MAP/RF4 Posts сначала принимаются в изо
Для измерений на собственном сервере подготовлен read-only `deploy/load-smoke.py`: он считает p50/p95/max и HTTP-коды для activity/records, а при наличии `ADMIN_TOKEN` — staging/moderation. Методика и безопасные ступени нагрузки описаны в [docs/load-testing.md](docs/load-testing.md); локальные цифры не выдаются за production baseline.
В production MinIO root credentials доступны только одноразовому init-контейнеру. API использует отдельного пользователя с доступом исключительно к `S3_BUCKET`: просмотр bucket, чтение, запись и удаление его объектов без глобального списка bucket и без права создавать новые.
После повторных ошибок scheduler увеличивает паузу экспоненциально до 24 часов и возвращается к 30 минутам после успеха. Публичная страница `/status` показывает свежесть и состояние источников без URL запросов, внутренних ошибок и другой диагностической информации.
Подробный план и актуальные чекбоксы находятся в [`docs/ROADMAP.md`](docs/ROADMAP.md). Результаты проверки интерфейса и пять приоритетных UX-пакетов описаны в [`docs/UI_UX_AUDIT.md`](docs/UI_UX_AUDIT.md).
+2 -1
View File
@@ -6,6 +6,7 @@ from typing import Any
from sqlalchemy import func, select, text
from sqlalchemy.orm import Session
from .config import settings
from .models import CommunityImportRun, DataSource, ImportStatus, OfficialRecordImport
@@ -32,7 +33,7 @@ def readiness_report(
ready = False
try:
s3.list_buckets()
s3.head_bucket(Bucket=settings.s3_bucket)
components["minio"] = {"status": "ready"}
except Exception:
components["minio"] = {"status": "unavailable"}
-4
View File
@@ -6,7 +6,6 @@ from functools import lru_cache
import boto3
from botocore.client import BaseClient
from botocore.exceptions import ClientError
from PIL import Image, UnidentifiedImageError
from .config import settings
@@ -63,10 +62,7 @@ def upload_screenshot(raw: bytes, *, filename: str | None = None, content_type:
body, extension, mime = prepare_image(raw)
key = f"reports/{uuid.uuid4()}.{extension}"
s3 = client()
try:
s3.head_bucket(Bucket=settings.s3_bucket)
except ClientError:
s3.create_bucket(Bucket=settings.s3_bucket)
s3.put_object(Bucket=settings.s3_bucket, Key=key, Body=body, ContentType=mime)
return key
+4 -3
View File
@@ -11,12 +11,13 @@ from app.readiness import readiness_report
class AvailableStorage:
def list_buckets(self) -> dict[str, list[object]]:
return {"Buckets": []}
def head_bucket(self, *, Bucket: str) -> dict[str, object]:
assert Bucket
return {}
class UnavailableStorage:
def list_buckets(self) -> None:
def head_bucket(self, *, Bucket: str) -> None:
raise ConnectionError("fixture unavailable")
+9 -1
View File
@@ -85,13 +85,21 @@ services:
test "$$MINIO_ROOT_USER" != "$$S3_ACCESS_KEY" &&
test "$$MINIO_ROOT_PASSWORD" != "$$S3_SECRET_KEY" &&
mc alias set local http://minio:9000 "$$MINIO_ROOT_USER" "$$MINIO_ROOT_PASSWORD" >/dev/null &&
mc mb --ignore-existing "local/$$S3_BUCKET" >/dev/null &&
printf '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:GetBucketLocation","s3:ListBucket"],"Resource":["arn:aws:s3:::%s"]},{"Effect":"Allow","Action":["s3:GetObject","s3:PutObject","s3:DeleteObject"],"Resource":["arn:aws:s3:::%s/*"]}]}' "$$S3_BUCKET" "$$S3_BUCKET" > /tmp/rf4-app-policy.json &&
mc admin policy create local rf4-app-bucket /tmp/rf4-app-policy.json >/dev/null &&
(mc admin user info local "$$S3_ACCESS_KEY" >/dev/null 2>&1 || mc admin user add local "$$S3_ACCESS_KEY" "$$S3_SECRET_KEY") &&
mc admin policy attach local readwrite --user "$$S3_ACCESS_KEY" >/dev/null
(mc admin policy detach local readwrite --user "$$S3_ACCESS_KEY" >/dev/null 2>&1 || true) &&
mc admin policy attach local rf4-app-bucket --user "$$S3_ACCESS_KEY" >/dev/null &&
mc alias set app http://minio:9000 "$$S3_ACCESS_KEY" "$$S3_SECRET_KEY" >/dev/null &&
mc stat "app/$$S3_BUCKET" >/dev/null &&
! mc ls app >/dev/null 2>&1
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?Set MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?Set MINIO_ROOT_PASSWORD}
S3_ACCESS_KEY: ${S3_ACCESS_KEY:?Set S3_ACCESS_KEY}
S3_SECRET_KEY: ${S3_SECRET_KEY:?Set S3_SECRET_KEY}
S3_BUCKET: ${S3_BUCKET:-catch-screenshots}
depends_on:
minio: {condition: service_healthy}
networks: [backend]
+1 -1
View File
@@ -21,7 +21,7 @@ openssl rand -base64 36 # отдельные значения для БД, ADMI
docker run --rm caddy:2.10.2-alpine caddy hash-password --plaintext 'ОТДЕЛЬНЫЙ ADMIN-ПАРОЛЬ'
```
Заполните `.env.production`. Хеш Caddy содержит символы `$`, поэтому значение `ADMIN_BASIC_PASSWORD_HASH` в env-файле заключите в одинарные кавычки. `MINIO_ROOT_*` и `S3_*` обязаны быть разными: init-контейнер проверяет это, API получает только application credentials. Если пароль PostgreSQL содержит специальные символы, в `DATABASE_URL` нужна URL-кодированная форма того же пароля. `.env.production` нельзя коммитить или пересылать вместе с логами.
Заполните `.env.production`. Хеш Caddy содержит символы `$`, поэтому значение `ADMIN_BASIC_PASSWORD_HASH` в env-файле заключите в одинарные кавычки. `MINIO_ROOT_*` и `S3_*` обязаны быть разными: init-контейнер root-доступом создаёт `S3_BUCKET`, назначает приложению только list/location этого bucket и get/put/delete его объектов, затем проверяет отсутствие глобального list. API получает только application credentials и не может создавать bucket. Если пароль PostgreSQL содержит специальные символы, в `DATABASE_URL` нужна URL-кодированная форма того же пароля. `.env.production` нельзя коммитить или пересылать вместе с логами.
## 3. Проверка и первый запуск
+1 -1
View File
@@ -25,7 +25,7 @@ export BACKUP_DIRECTORY="$backup_root"
env_file=${COMPOSE_ENV_FILE:-.env.production}
compose="docker compose --env-file $env_file -f compose.production.yaml"
$compose exec -T db sh -c 'pg_dump --format=custom --no-owner --no-acl -U "$POSTGRES_USER" -d "$POSTGRES_DB"' > "$target/postgres.dump"
$compose run --rm --entrypoint /bin/sh storage-tool -c 'mc alias set local http://minio:9000 "$S3_ACCESS_KEY" "$S3_SECRET_KEY" >/dev/null && mc mb --ignore-existing "local/$S3_BUCKET" >/dev/null && mc mirror --overwrite "local/$S3_BUCKET" "/backup/'"$stamp"'/minio" && chmod -R a+rwX "/backup/'"$stamp"'/minio"'
$compose run --rm --entrypoint /bin/sh storage-tool -c 'mc alias set local http://minio:9000 "$S3_ACCESS_KEY" "$S3_SECRET_KEY" >/dev/null && mc stat "local/$S3_BUCKET" >/dev/null && mc mirror --overwrite "local/$S3_BUCKET" "/backup/'"$stamp"'/minio" && chmod -R a+rwX "/backup/'"$stamp"'/minio"'
tar -czf "$target/minio.tar.gz" -C "$target" minio
rm -rf "$target/minio"
(cd "$target" && sha256sum postgres.dump minio.tar.gz > SHA256SUMS)
+1 -1
View File
@@ -26,6 +26,6 @@ env_file=${COMPOSE_ENV_FILE:-.env.production}
compose="docker compose --env-file $env_file -f compose.production.yaml"
$compose stop api web
$compose exec -T db sh -c 'pg_restore --clean --if-exists --exit-on-error --no-owner --no-acl -U "$POSTGRES_USER" -d "$POSTGRES_DB"' < "$source_dir/postgres.dump"
$compose run --rm --entrypoint /bin/sh storage-tool -c 'mc alias set local http://minio:9000 "$S3_ACCESS_KEY" "$S3_SECRET_KEY" >/dev/null && mc rm --recursive --force "local/$S3_BUCKET" >/dev/null 2>&1 || true; mc mb --ignore-existing "local/$S3_BUCKET" >/dev/null && mc mirror --overwrite "/backup/'"$restore_name"'/minio" "local/$S3_BUCKET"'
$compose run --rm --entrypoint /bin/sh storage-tool -c 'mc alias set local http://minio:9000 "$S3_ACCESS_KEY" "$S3_SECRET_KEY" >/dev/null && mc stat "local/$S3_BUCKET" >/dev/null && mc rm --recursive --force "local/$S3_BUCKET" >/dev/null 2>&1 && mc mirror --overwrite "/backup/'"$restore_name"'/minio" "local/$S3_BUCKET"'
$compose up -d ${RESTORE_START_SERVICES:-minio api web proxy}
echo "Restore completed from: $source_dir"
+1 -1
View File
@@ -19,7 +19,7 @@ trap cleanup EXIT INT TERM
$compose up -d --wait db minio
$compose run --rm minio-init
$compose exec -T db sh -c 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "create table restore_drill (value text primary key); insert into restore_drill values ('"'"'database-ok'"'"');"' >/dev/null
$compose run --rm --entrypoint /bin/sh storage-tool -c 'mc alias set local http://minio:9000 "$S3_ACCESS_KEY" "$S3_SECRET_KEY" >/dev/null; mc mb --ignore-existing "local/$S3_BUCKET" >/dev/null; printf object-ok | mc pipe "local/$S3_BUCKET/restore-drill.txt"' >/dev/null
$compose run --rm --entrypoint /bin/sh storage-tool -c 'mc alias set local http://minio:9000 "$S3_ACCESS_KEY" "$S3_SECRET_KEY" >/dev/null; mc stat "local/$S3_BUCKET" >/dev/null; printf object-ok | mc pipe "local/$S3_BUCKET/restore-drill.txt"' >/dev/null
./deploy/backup.sh "$drill_root"
backup=$(find "$drill_root" -mindepth 1 -maxdepth 1 -type d ! -name '.rf4-restore.*' | head -n 1)
+1 -1
View File
@@ -43,7 +43,7 @@
- [x] **Q05 · Базовая визуальная матрица.** Главная проверена в браузере на 320/390/768/1280 px, ключевые public-маршруты — на 320 px; удалён корневой `min-width`, создававший горизонтальный scroll. Добавлен E2E-контракт для `/`, records, report, waterbodies, status и видимого skip-link. Ширина 320 px также покрывает reflow, эквивалентный 200% zoom для окна 640 px. Расширенная матрица наполненных/длинных/error-состояний остаётся постоянной частью приёмки UI, а не отдельным блокером.
- [x] **Q06 · Performance baseline.** На локальной production-сборке после оптимизации hero: performance 100, LCP 1,66 с, FCP 1,15 с, CLS 0,023, TBT 9 мс. Устранены найденные Lighthouse проблемы контраста и accessible name; методика и бюджеты записаны в [performance-baseline.md](performance-baseline.md). Полевой INP измеряется только после запуска.
- [x] **Q07 · Нагрузочная методика.** Добавлен read-only runner для activity, records, staging и moderation с warm-up, p50/p95/max, распределением HTTP-кодов и ограниченной concurrency. Методика фиксирует контекст запуска, ступени нагрузки и бюджеты, но не объявляет результатов до трёх прогонов на целевом сервере.
- [ ] **Q08 · Политика MinIO.** Ограничить app credentials одним bucket и добавить безопасную автоматическую проверку policy; root credentials оставить только bootstrap-задаче.
- [x] **Q08 · Политика MinIO.** Production bootstrap создаёт bucket и отдельную policy только с bucket location/list и get/put/delete его объектов. App credentials проверяются через bucket stat и отрицательную проверку глобального list; API больше не требует `ListAllMyBuckets` и не пытается создавать bucket. Root credentials остаются только у init-задачи.
- [ ] **Q09 · Release-процедура.** Разделить миграционный/release-шаг и запуск приложения либо документировать выбранную стратегию отката; проверить upgrade с предыдущей ревизии на копии данных.
- [ ] **Q10 · Документальная ревизия.** После каждого пакета обновлять этот файл и README, не возвращая закрытые R/A/T-задачи в активный backlog.