Law/scripts/ops/deploy_prod.sh
2026-03-02 16:22:07 +03:00

188 lines
6.4 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT_DIR"
if [[ ! -f .env ]]; then
echo "[ERROR] .env not found in $ROOT_DIR"
exit 1
fi
read_env_var() {
local key="$1"
local value
value="$(grep -E "^${key}=" .env | tail -n1 | cut -d= -f2- || true)"
echo "$value"
}
is_truthy() {
local value="${1:-}"
[[ "$value" == "true" || "$value" == "1" ]]
}
is_insecure_secret() {
local value="${1:-}"
local lowered
lowered="$(echo "$value" | tr '[:upper:]' '[:lower:]')"
if [[ -z "$value" || "${#value}" -lt 24 ]]; then
return 0
fi
if [[ "$lowered" == *"change_me"* || "$lowered" == *"admin123"* || "$lowered" == *"password"* || "$lowered" == *"example"* ]]; then
return 0
fi
return 1
}
contains_localhost_origin() {
local csv="${1:-}"
local normalized
normalized="$(echo "$csv" | tr '[:upper:]' '[:lower:]')"
[[ "$normalized" == *"localhost"* || "$normalized" == *"127.0.0.1"* ]]
}
fail_if_insecure_env() {
local app_env otp_dev bootstrap cookie_secure s3_ssl s3_verify s3_endpoint s3_ca_path strict_origin
local chat_secret minio_user minio_password
local minio_tls_enabled
local admin_jwt public_jwt data_secret data_kid data_keys chat_kid chat_keys internal_token
local public_allowed cors_origins admin_auth_mode
app_env="$(read_env_var APP_ENV)"
otp_dev="$(read_env_var OTP_DEV_MODE)"
bootstrap="$(read_env_var ADMIN_BOOTSTRAP_ENABLED)"
cookie_secure="$(read_env_var PUBLIC_COOKIE_SECURE)"
s3_ssl="$(read_env_var S3_USE_SSL)"
s3_verify="$(read_env_var S3_VERIFY_SSL)"
s3_endpoint="$(read_env_var S3_ENDPOINT)"
s3_ca_path="$(read_env_var S3_CA_CERT_PATH)"
strict_origin="$(read_env_var PUBLIC_STRICT_ORIGIN_CHECK)"
public_allowed="$(read_env_var PUBLIC_ALLOWED_WEB_ORIGINS)"
cors_origins="$(read_env_var CORS_ORIGINS)"
admin_auth_mode="$(read_env_var ADMIN_AUTH_MODE)"
chat_secret="$(read_env_var CHAT_ENCRYPTION_SECRET)"
admin_jwt="$(read_env_var ADMIN_JWT_SECRET)"
public_jwt="$(read_env_var PUBLIC_JWT_SECRET)"
data_secret="$(read_env_var DATA_ENCRYPTION_SECRET)"
data_kid="$(read_env_var DATA_ENCRYPTION_ACTIVE_KID)"
data_keys="$(read_env_var DATA_ENCRYPTION_KEYS)"
chat_kid="$(read_env_var CHAT_ENCRYPTION_ACTIVE_KID)"
chat_keys="$(read_env_var CHAT_ENCRYPTION_KEYS)"
internal_token="$(read_env_var INTERNAL_SERVICE_TOKEN)"
minio_user="$(read_env_var MINIO_ROOT_USER)"
minio_password="$(read_env_var MINIO_ROOT_PASSWORD)"
minio_tls_enabled="$(read_env_var MINIO_TLS_ENABLED)"
if [[ "$app_env" != "prod" && "$app_env" != "production" ]]; then
echo "[WARN] APP_ENV is '$app_env' (expected: prod)"
fi
if is_truthy "$otp_dev"; then
echo "[ERROR] OTP_DEV_MODE must be false for production"
exit 1
fi
if is_truthy "$bootstrap"; then
echo "[ERROR] ADMIN_BOOTSTRAP_ENABLED must be false for production"
exit 1
fi
if ! is_truthy "$cookie_secure"; then
echo "[ERROR] PUBLIC_COOKIE_SECURE must be true for production"
exit 1
fi
if ! is_truthy "$s3_ssl"; then
echo "[ERROR] S3_USE_SSL must be true for production"
exit 1
fi
if ! is_truthy "$s3_verify"; then
echo "[ERROR] S3_VERIFY_SSL must be true for production"
exit 1
fi
if [[ "${s3_endpoint,,}" != https://* ]]; then
echo "[ERROR] S3_ENDPOINT must start with https:// in production"
exit 1
fi
if [[ -z "$s3_ca_path" ]]; then
echo "[ERROR] S3_CA_CERT_PATH must be configured for trusted internal TLS"
exit 1
fi
if ! is_truthy "$minio_tls_enabled"; then
echo "[ERROR] MINIO_TLS_ENABLED must be true for production"
exit 1
fi
if ! is_truthy "$strict_origin"; then
echo "[ERROR] PUBLIC_STRICT_ORIGIN_CHECK must be true for production"
exit 1
fi
if contains_localhost_origin "$public_allowed"; then
echo "[ERROR] PUBLIC_ALLOWED_WEB_ORIGINS must not include localhost/127.0.0.1 in production"
exit 1
fi
if contains_localhost_origin "$cors_origins"; then
echo "[ERROR] CORS_ORIGINS must not include localhost/127.0.0.1 in production"
exit 1
fi
if [[ "$admin_auth_mode" != "password_totp_required" ]]; then
echo "[ERROR] ADMIN_AUTH_MODE must be password_totp_required in production"
exit 1
fi
if is_insecure_secret "$chat_secret"; then
echo "[ERROR] CHAT_ENCRYPTION_SECRET must be configured and non-default"
exit 1
fi
if is_insecure_secret "$admin_jwt"; then
echo "[ERROR] ADMIN_JWT_SECRET must be configured and strong"
exit 1
fi
if is_insecure_secret "$public_jwt"; then
echo "[ERROR] PUBLIC_JWT_SECRET must be configured and strong"
exit 1
fi
if is_insecure_secret "$data_secret"; then
echo "[ERROR] DATA_ENCRYPTION_SECRET must be configured and strong"
exit 1
fi
if [[ -z "$data_kid" ]]; then
echo "[ERROR] DATA_ENCRYPTION_ACTIVE_KID must be set"
exit 1
fi
if [[ -n "$data_keys" && "$data_keys" != *"${data_kid}="* ]]; then
echo "[ERROR] DATA_ENCRYPTION_KEYS must contain active kid (${data_kid}=...)"
exit 1
fi
if [[ -n "$chat_kid" && -n "$chat_keys" && "$chat_keys" != *"${chat_kid}="* ]]; then
echo "[ERROR] CHAT_ENCRYPTION_KEYS must contain active kid (${chat_kid}=...)"
exit 1
fi
if is_insecure_secret "$internal_token"; then
echo "[ERROR] INTERNAL_SERVICE_TOKEN must be configured and strong"
exit 1
fi
if [[ -z "$minio_user" || "$minio_user" == "minioadmin" || "$minio_user" == "minio_local_admin" ]]; then
echo "[ERROR] MINIO_ROOT_USER must be set to non-default value"
exit 1
fi
if is_insecure_secret "$minio_password" || [[ "$minio_password" == "minioadmin" ]]; then
echo "[ERROR] MINIO_ROOT_PASSWORD must be set to non-default value"
exit 1
fi
if [[ ! -f "deploy/tls/minio/public.crt" || ! -f "deploy/tls/minio/private.key" || ! -f "deploy/tls/minio/ca.crt" ]]; then
echo "[ERROR] MinIO TLS cert bundle is missing. Run: ./scripts/ops/minio_tls_bootstrap.sh"
exit 1
fi
}
fail_if_insecure_env
echo "[1/4] Build and start production stack..."
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
echo "[2/4] Apply migrations..."
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec -T backend alembic upgrade head
echo "[3/4] Service status..."
docker compose -f docker-compose.yml -f docker-compose.prod.yml ps
echo "[4/4] Smoke checks..."
curl -fsS http://localhost/health >/dev/null
curl -fsS http://localhost/chat-health >/dev/null
curl -fsS http://localhost/email-health >/dev/null
echo "Done. Open https://ruakb.ru"