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

170 lines
5.3 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT_DIR"
DOMAIN="${DOMAIN:-ruakb.ru}"
WWW_DOMAIN="${WWW_DOMAIN:-www.ruakb.ru}"
SECOND_DOMAIN="${SECOND_DOMAIN:-ruakb.online}"
SECOND_WWW_DOMAIN="${SECOND_WWW_DOMAIN:-www.ruakb.online}"
LETSENCRYPT_EMAIL="${LETSENCRYPT_EMAIL:-admin@ruakb.ru}"
AUTO_CERT_INIT="${AUTO_CERT_INIT:-0}"
PROD_COMPOSE=(docker compose -f docker-compose.yml -f docker-compose.prod.nginx.yml)
CERT_COMPOSE=(docker compose -f docker-compose.yml -f docker-compose.prod.nginx.yml -f docker-compose.prod.cert.yml)
log() {
echo "[SEC-AUDIT] $*"
}
warn() {
echo "[SEC-AUDIT][WARN] $*" >&2
}
fail() {
echo "[SEC-AUDIT][ERROR] $*" >&2
exit 1
}
file_missing() {
[[ ! -f "$1" ]]
}
ensure_env_file() {
if [[ -f ".env" ]]; then
log ".env found"
return 0
fi
if [[ -f ".env.prod" ]]; then
cp .env.prod .env
chmod 600 .env
log ".env was missing -> restored from .env.prod"
return 0
fi
if [[ -f ".env.production" ]]; then
log ".env/.env.prod missing -> generating .env.prod from .env.production"
./scripts/ops/rotate_prod_secrets.sh --env-in .env.production --env-out .env.prod
cp .env.prod .env
chmod 600 .env
log ".env created from generated .env.prod"
return 0
fi
fail "Cannot build .env automatically: missing both .env.prod and .env.production"
}
ensure_minio_tls_bundle() {
if file_missing "deploy/tls/minio/public.crt" || file_missing "deploy/tls/minio/private.key" || file_missing "deploy/tls/minio/ca.crt"; then
log "MinIO TLS bundle is missing -> generating"
./scripts/ops/minio_tls_bootstrap.sh
else
log "MinIO TLS bundle present"
fi
}
ensure_compose_files() {
file_missing "docker-compose.prod.nginx.yml" && fail "Missing docker-compose.prod.nginx.yml"
file_missing "docker-compose.prod.cert.yml" && fail "Missing docker-compose.prod.cert.yml"
file_missing "frontend/nginx.prod.conf" && fail "Missing frontend/nginx.prod.conf"
file_missing "deploy/nginx/edge-http-only.conf" && fail "Missing deploy/nginx/edge-http-only.conf"
file_missing "deploy/nginx/edge-https.conf" && fail "Missing deploy/nginx/edge-https.conf"
log "Compose/nginx files present"
}
stack_up_and_migrate() {
log "Starting production stack (nginx profile)"
"${PROD_COMPOSE[@]}" up -d --build --remove-orphans db redis minio backend chat-service email-service worker beat frontend edge clamav
log "Applying migrations"
"${PROD_COMPOSE[@]}" exec -T backend alembic upgrade head
}
run_security_preflight() {
log "Running production security preflight (app-level config validation)"
"${PROD_COMPOSE[@]}" run --rm --no-deps backend python - <<'PY'
from app.core.config import validate_production_security_or_raise
validate_production_security_or_raise("prod-security-audit")
print("production security config validation: ok")
PY
}
run_local_smoke() {
log "Running local smoke checks via localhost"
./scripts/ops/check_chat_health.sh http://localhost >/dev/null
./scripts/ops/security_smoke.sh http://localhost >/dev/null
log "Local smoke checks passed"
}
https_health_ok() {
local url="$1"
local code
code="$(curl -k -sS -o /dev/null -w "%{http_code}" "${url%/}/health" || true)"
[[ "$code" == "200" ]]
}
cert_bootstrap() {
log "AUTO_CERT_INIT=1 and https health failed -> running cert bootstrap"
"${CERT_COMPOSE[@]}" up -d --build db redis minio backend chat-service email-service worker beat frontend edge
"${CERT_COMPOSE[@]}" run --rm certbot certonly --webroot -w /var/www/certbot \
--email "$LETSENCRYPT_EMAIL" --agree-tos --no-eff-email --non-interactive --expand \
-d "$DOMAIN" -d "$WWW_DOMAIN" -d "$SECOND_DOMAIN" -d "$SECOND_WWW_DOMAIN"
"${PROD_COMPOSE[@]}" up -d --build edge
}
run_domain_smoke() {
local domain="$1"
[[ -z "$domain" ]] && return 0
local url="https://${domain}"
if ! https_health_ok "$url"; then
if [[ "$AUTO_CERT_INIT" == "1" ]]; then
cert_bootstrap
https_health_ok "$url" || fail "HTTPS health still failing after cert bootstrap: ${url}/health"
else
fail "HTTPS health check failed: ${url}/health (set AUTO_CERT_INIT=1 to auto-bootstrap certs)"
fi
fi
log "Running security smoke for $url"
./scripts/ops/security_smoke.sh "$url" >/dev/null
}
run_incident_report() {
log "Generating incident checklist snapshot"
./scripts/ops/incident_checklist.sh \
--severity LOW \
--category MONITORING_ALERT \
--summary "Scheduled production security audit completed"
}
print_summary() {
log "Collecting final status"
"${PROD_COMPOSE[@]}" ps
local latest_security_report
latest_security_report="$(ls -1t reports/security/security-smoke-*.md 2>/dev/null | head -n 1 || true)"
local latest_incident_report
latest_incident_report="$(ls -1t reports/incidents/incident-*.md 2>/dev/null | head -n 1 || true)"
[[ -n "$latest_security_report" ]] && log "Latest security smoke report: $latest_security_report"
[[ -n "$latest_incident_report" ]] && log "Latest incident checklist: $latest_incident_report"
}
main() {
ensure_compose_files
ensure_env_file
ensure_minio_tls_bundle
stack_up_and_migrate
run_security_preflight
run_local_smoke
run_domain_smoke "$DOMAIN"
run_domain_smoke "$SECOND_DOMAIN"
run_incident_report
print_summary
log "Production security audit completed successfully"
}
main "$@"