chore(home): mirror from kalinamall (9883e6a) with papatramp URLs
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
# Только для Docker Compose (см. docs/install-ubuntu-24.04-docker.md).
|
||||
# Production native: deploy/env.native.example → /opt/security-alert-center/config/sac-api.env
|
||||
|
||||
# PostgreSQL
|
||||
POSTGRES_USER=sac
|
||||
POSTGRES_PASSWORD=change-me-long-random
|
||||
POSTGRES_DB=sac
|
||||
|
||||
# API (публичный URL для ссылок в ответах ingest)
|
||||
SAC_PUBLIC_URL=https://sac.kalinamall.ru
|
||||
SAC_API_PORT=8000
|
||||
|
||||
# JWT (UI — позже)
|
||||
JWT_SECRET=change-me-openssl-rand-hex-32
|
||||
|
||||
# Первый API key для агентов (заголовок: Authorization: Bearer <ключ>)
|
||||
# Сгенерировать: python -c "import secrets; print('sac_'+secrets.token_urlsafe(32))"
|
||||
SAC_BOOTSTRAP_API_KEY=sac_replace_with_generated_key
|
||||
|
||||
# Admin UI (фаза 1C)
|
||||
SAC_ADMIN_USERNAME=admin
|
||||
SAC_ADMIN_PASSWORD=
|
||||
|
||||
# Telegram notifications from SAC (phase 1C.4)
|
||||
TELEGRAM_ENABLED=false
|
||||
TELEGRAM_BOT_TOKEN=
|
||||
TELEGRAM_CHAT_ID=
|
||||
TELEGRAM_MIN_SEVERITY=high
|
||||
|
||||
CORS_ORIGINS=*
|
||||
@@ -0,0 +1,30 @@
|
||||
# Deploy
|
||||
|
||||
## Production (native) — основной путь
|
||||
|
||||
1. [docs/install-ubuntu-24.04-native.md](../docs/install-ubuntu-24.04-native.md)
|
||||
2. `deploy/env.native.example` → `/opt/security-alert-center/config/sac-api.env`
|
||||
3. `deploy/systemd/sac-api.service` → `/etc/systemd/system/`
|
||||
4. `deploy/nginx/sac.conf.tls.example` → `/etc/nginx/sites-available/sac` (wildcard TLS; только `sac.kalinamall.ru`, чужие Host → 444)
|
||||
или `sac.conf.example` — только HTTP / отладка
|
||||
5. `deploy/sac-deploy.sh` → `/opt/sac-deploy.sh` — полное обновление после `git pull`
|
||||
|
||||
```bash
|
||||
sudo cp deploy/sac-deploy.sh /opt/sac-deploy.sh
|
||||
sudo chmod 755 /opt/sac-deploy.sh
|
||||
sudo /opt/sac-deploy.sh
|
||||
```
|
||||
|
||||
Суточные отчёты: `sac-daily-report.service` + timer. Файл `sac-daily-report.timer` в git — шаблон;
|
||||
на сервере timer **генерируется** из `SAC_DAILY_REPORT_HOUR` / `SAC_DAILY_REPORT_TIMEZONE`
|
||||
(`deploy/systemd/render-sac-daily-report-timer.sh`, вызывается из `sac-deploy.sh`).
|
||||
|
||||
## Альтернатива: Docker
|
||||
|
||||
Только для стенда. См. [docs/install-ubuntu-24.04-docker.md](../docs/install-ubuntu-24.04-docker.md).
|
||||
|
||||
```bash
|
||||
cp .env.example .env # docker-only
|
||||
docker compose up -d --build
|
||||
docker compose run --rm migrate
|
||||
```
|
||||
@@ -0,0 +1,51 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-bookworm
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-sac}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-sac}
|
||||
volumes:
|
||||
- sac_pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-sac} -d ${POSTGRES_DB:-sac}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
api:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: backend/Dockerfile
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
DATABASE_URL: postgresql+psycopg2://${POSTGRES_USER:-sac}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-sac}
|
||||
SAC_PUBLIC_URL: ${SAC_PUBLIC_URL:-http://localhost:8000}
|
||||
JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET}
|
||||
SAC_BOOTSTRAP_API_KEY: ${SAC_BOOTSTRAP_API_KEY:?set SAC_BOOTSTRAP_API_KEY}
|
||||
EVENT_SCHEMA_PATH: /app/schemas/event-schema-v1.json
|
||||
CORS_ORIGINS: ${CORS_ORIGINS:-*}
|
||||
ports:
|
||||
- "${SAC_API_PORT:-8000}:8000"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
migrate:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: backend/Dockerfile
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
DATABASE_URL: postgresql+psycopg2://${POSTGRES_USER:-sac}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-sac}
|
||||
command: alembic upgrade head
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
sac_pgdata:
|
||||
@@ -0,0 +1,139 @@
|
||||
# Native: /opt/security-alert-center/config/sac-api.env
|
||||
# sudo chown sac:sac ... && sudo chmod 600 ...
|
||||
# Пароль в URL — в двойных кавычках. Символ # в пароле допустим только внутри кавычек.
|
||||
# systemd НЕ парсит этот файл — читает приложение (SAC_CONFIG_FILE).
|
||||
|
||||
DATABASE_URL=postgresql+psycopg2://sac:CHANGE_ME_POSTGRES_PASSWORD@127.0.0.1:5432/sac
|
||||
# SQLAlchemy pool (uvicorn workers × concurrent ingest). Было по умолчанию 5+10 — мало для штурма 09:00.
|
||||
SAC_DB_POOL_SIZE=15
|
||||
SAC_DB_MAX_OVERFLOW=25
|
||||
# Uvicorn workers (ingest burst). Читает deploy/systemd/sac-api-start.sh; при >1 отключите in-process host_silence scan ниже.
|
||||
SAC_UVICORN_WORKERS=4
|
||||
|
||||
SAC_PUBLIC_URL=https://sac.kalinamall.ru
|
||||
# Опционально: другой базовый URL для WinRM-скачивания RDP bundle с ПК (LAN / split-DNS).
|
||||
# SAC_AGENT_BUNDLE_BASE_URL=https://192.168.x.x
|
||||
JWT_SECRET=CHANGE_ME_openssl_rand_hex_32
|
||||
SAC_SECURITY_ENFORCE=true
|
||||
|
||||
# API key для агентов: Authorization: Bearer <ключ>
|
||||
# python3.12 -c "import secrets; print('sac_'+secrets.token_urlsafe(32))"
|
||||
SAC_BOOTSTRAP_API_KEY=sac_replace_with_generated_key
|
||||
|
||||
EVENT_SCHEMA_PATH=/opt/security-alert-center/schemas/event-schema-v1.json
|
||||
|
||||
SAC_ADMIN_USERNAME=admin
|
||||
SAC_ADMIN_PASSWORD=
|
||||
|
||||
# Telegram notifications from SAC (phase 1C.4)
|
||||
# При UseSAC=exclusive на агентах — основной канал для оператора; см. docs/agent-integration.md §1.3.1
|
||||
TELEGRAM_ENABLED=false
|
||||
TELEGRAM_BOT_TOKEN=
|
||||
TELEGRAM_CHAT_ID=
|
||||
# exclusive: warning (failed login, problems); prod noise-only: high
|
||||
TELEGRAM_MIN_SEVERITY=high
|
||||
|
||||
# Generic webhook (JSON POST), F-NOT-01
|
||||
WEBHOOK_ENABLED=false
|
||||
WEBHOOK_URL=
|
||||
WEBHOOK_SECRET_HEADER=
|
||||
WEBHOOK_SECRET=
|
||||
WEBHOOK_MIN_SEVERITY=high
|
||||
|
||||
# SMTP email (F-NOT-01)
|
||||
SMTP_ENABLED=false
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_FROM=
|
||||
SMTP_TO=
|
||||
SMTP_STARTTLS=true
|
||||
SMTP_SSL=false
|
||||
SMTP_MIN_SEVERITY=high
|
||||
|
||||
# Глобальное правило: severity ≥ NOTIFY_MIN_SEVERITY → каналы из NOTIFY_CHANNELS
|
||||
NOTIFY_MIN_SEVERITY=warning
|
||||
NOTIFY_CHANNELS=telegram,webhook,email
|
||||
|
||||
# Cooldown оповещений SAC (секунды; 0 = выкл. для kind)
|
||||
SAC_NOTIFY_COOLDOWN_ENABLED=true
|
||||
SAC_NOTIFY_EVENT_COOLDOWN_SEC=90
|
||||
SAC_NOTIFY_PROBLEM_COOLDOWN_SEC=300
|
||||
|
||||
# Суточные отчёты из SAC (F-NOT-05; systemd sac-daily-report.timer)
|
||||
SAC_DAILY_REPORT_ENABLED=true
|
||||
SAC_DAILY_REPORT_HOUR=9
|
||||
SAC_DAILY_REPORT_TIMEZONE=Europe/Moscow
|
||||
SAC_DAILY_REPORT_SKIP_IF_AGENT_SENT=true
|
||||
SAC_DAILY_REPORT_REQUIRE_ACTIVITY=true
|
||||
|
||||
# Статус хоста по agent.heartbeat
|
||||
# Порог stale для agent.heartbeat (мин). При интервале агента ~4 ч — 300 (5 ч) даёт запас.
|
||||
SAC_HEARTBEAT_STALE_MINUTES=300
|
||||
|
||||
# Проактивный алерт host_silence (без ожидания нового события с хоста)
|
||||
# Если включён systemd timer sac-host-silence-scan.timer — лучше SAC_HOST_SILENCE_SCAN_ENABLED=false
|
||||
# (иначе scan идёт параллельно из каждого uvicorn worker + timer).
|
||||
SAC_HOST_SILENCE_SCAN_ENABLED=true
|
||||
SAC_HOST_SILENCE_SCAN_INTERVAL_MINUTES=5
|
||||
|
||||
# Корреляция Problems: host + type + rule в одном open Problem (минуты)
|
||||
SAC_PROBLEM_CORRELATION_WINDOW_MINUTES=60
|
||||
|
||||
# rule:brute_force_burst (ssh.login.failed / rdp.login.failed)
|
||||
SAC_BRUTE_FORCE_WINDOW_MINUTES=15
|
||||
SAC_BRUTE_FORCE_THRESHOLD=30
|
||||
|
||||
# rule:privilege_spike (privilege.sudo.command)
|
||||
SAC_PRIVILEGE_SPIKE_WINDOW_MINUTES=10
|
||||
SAC_PRIVILEGE_SPIKE_THRESHOLD=10
|
||||
|
||||
# rule:rdg_session_flap (302→303)
|
||||
SAC_RDG_FLAP_WINDOW_MIN_SEC=1
|
||||
SAC_RDG_FLAP_WINDOW_MAX_SEC=10
|
||||
SAC_RDG_FLAP_DEDUP_SEC=30
|
||||
|
||||
# IP HAProxy (или другого RDG-прокси), через который виден вход: external_ip в событии 302/303
|
||||
# SAC_RDG_HAPROXY_EXTERNAL_IPS=192.168.160.50,10.0.0.100
|
||||
|
||||
# qwinsta/logoff на Windows-хостах (доменный admin)
|
||||
# SAC_WIN_ADMIN_USER=B26\\Administrator
|
||||
# SAC_WIN_ADMIN_PASSWORD=
|
||||
|
||||
# SSH / update_ssh_monitor.sh на Linux-хостах
|
||||
# SAC_LINUX_ADMIN_USER=root
|
||||
# SAC_LINUX_ADMIN_PASSWORD=
|
||||
|
||||
# Retention (systemd sac-retention.timer)
|
||||
SAC_EVENTS_RETENTION_DAYS=90
|
||||
SAC_PROBLEMS_RETENTION_DAYS=180
|
||||
|
||||
# UI login: блокировка IP после N неудачных попыток (окно в минутах) + алерт в Telegram
|
||||
SAC_LOGIN_MAX_FAILURES=3
|
||||
SAC_LOGIN_FAILURE_WINDOW_MINUTES=15
|
||||
SAC_LOGIN_ALERT_TELEGRAM=true
|
||||
# IP через запятую — не блокируются в UI (дополняет список в Настройках SAC)
|
||||
SAC_LOGIN_IP_WHITELIST=192.168.160.3
|
||||
# fail2ban: синхронизация белого списка из UI (нужны права записи + reload)
|
||||
SAC_FAIL2BAN_SYNC_ENABLED=false
|
||||
SAC_FAIL2BAN_SSH_JAIL=sshd
|
||||
SAC_FAIL2BAN_IGNOREIP_FILE=/etc/fail2ban/jail.d/sac-login-whitelist.local
|
||||
|
||||
# Seaca mobile push (FCM HTTP v1)
|
||||
SAC_FCM_ENABLED=false
|
||||
SAC_FCM_PROJECT_ID=
|
||||
# Путь к JSON service account Firebase (chmod 600, владелец sac)
|
||||
SAC_FCM_SERVICE_ACCOUNT_JSON=
|
||||
SAC_MOBILE_REFRESH_EXPIRE_DAYS=90
|
||||
|
||||
CORS_ORIGINS=https://sac.kalinamall.ru
|
||||
|
||||
# SSH: verify host keys (RejectPolicy). Add keys to file before remote actions.
|
||||
# Комментарий — только отдельной строкой с #. Нельзя: SAC_SSH_AUTO_ADD_HOST_KEY=false ← текст
|
||||
# SAC_SSH_KNOWN_HOSTS_FILE=/opt/security-alert-center/config/ssh_known_hosts
|
||||
# SAC_SSH_AUTO_ADD_HOST_KEY=false
|
||||
|
||||
# WinRM: enable HTTPS listener on clients (5986) before turning on.
|
||||
# SAC_WINRM_USE_HTTPS=false
|
||||
# SAC_WINRM_SERVER_CERT_VALIDATION=validate
|
||||
@@ -0,0 +1,69 @@
|
||||
# /etc/nginx/sites-available/sac
|
||||
# Production FQDN: sac.kalinamall.ru
|
||||
# API: 127.0.0.1:8000 (systemd sac-api)
|
||||
#
|
||||
# Hostname: только sac.kalinamall.ru; остальные Host → 444 (см. default_server ниже).
|
||||
#
|
||||
# Порядок: 1) этот конфиг (только :80) → nginx -t → reload
|
||||
# 2) certbot --nginx -d sac.kalinamall.ru (добавит :443 и сертификаты)
|
||||
|
||||
upstream sac_api {
|
||||
server 127.0.0.1:8000;
|
||||
keepalive 8;
|
||||
}
|
||||
|
||||
# --- default: отклонить чужие Host (не sac.kalinamall.ru) ---
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name _;
|
||||
return 444;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name sac.kalinamall.ru;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/html;
|
||||
}
|
||||
|
||||
client_max_body_size 2m;
|
||||
|
||||
# SSH/WinRM и git-probe: backend ждёт до 900s
|
||||
location ~ ^/api/v1/(hosts/[0-9]+/actions/(agent-update|agent-update-fallback|ssh-test|winrm-test)|settings/agent-updates/test-git) {
|
||||
proxy_pass http://sac_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 960s;
|
||||
proxy_read_timeout 960s;
|
||||
}
|
||||
|
||||
# Ingest burst: lifecycle/auth deferred в API, но POST всё равно может ждать notify_event/problem.
|
||||
location = /api/v1/events {
|
||||
proxy_pass http://sac_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 120s;
|
||||
proxy_read_timeout 120s;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://sac_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
# /etc/nginx/sites-available/sac
|
||||
# HTTPS с корпоративным wildcard-сертификатом (например VeriSign / DigiCert).
|
||||
# Покрывает sac.kalinamall.ru при CN/SAN *.kalinamall.ru или sac.kalinamall.ru
|
||||
#
|
||||
# Hostname: только sac.kalinamall.ru. Любой другой Host (например ext.kalinamall.ru
|
||||
# при ошибке DNS/маршрутизации на IP SAC) получает 444 / reject handshake —
|
||||
# форма входа SAC не отдаётся на «чужом» имени.
|
||||
#
|
||||
# Перед включением:
|
||||
# 1) Положите fullchain и ключ в /etc/ssl/sac/ (см. docs/ssl-certificate.md)
|
||||
# 2) Подставьте пути в ssl_certificate / ssl_certificate_key ниже
|
||||
# 3) sudo nginx -t && sudo systemctl reload nginx
|
||||
|
||||
upstream sac_api {
|
||||
server 127.0.0.1:8000;
|
||||
keepalive 8;
|
||||
}
|
||||
|
||||
# --- default: отклонить чужие Host на :80 ---
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name _;
|
||||
return 444;
|
||||
}
|
||||
|
||||
# --- default: отклонить чужой SNI/Host на :443 (nginx >= 1.19.5) ---
|
||||
server {
|
||||
listen 443 ssl default_server;
|
||||
listen [::]:443 ssl default_server;
|
||||
server_name _;
|
||||
ssl_reject_handshake on;
|
||||
}
|
||||
|
||||
# Редирект HTTP → HTTPS (только sac.kalinamall.ru)
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name sac.kalinamall.ru;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name sac.kalinamall.ru;
|
||||
|
||||
# --- замените пути на ваши файлы wildcard ---
|
||||
ssl_certificate /etc/ssl/sac/fullchain.pem;
|
||||
ssl_certificate_key /etc/ssl/sac/privkey.pem;
|
||||
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SAC_SSL:10m;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
client_max_body_size 2m;
|
||||
|
||||
# SSH/WinRM и git-probe: backend ждёт до 900s
|
||||
location ~ ^/api/v1/(hosts/[0-9]+/actions/(agent-update|agent-update-fallback|ssh-test|winrm-test)|settings/agent-updates/test-git) {
|
||||
proxy_pass http://sac_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 960s;
|
||||
proxy_read_timeout 960s;
|
||||
}
|
||||
|
||||
# Ingest burst: lifecycle/auth deferred в API, но POST всё равно может ждать notify_event/problem.
|
||||
location = /api/v1/events {
|
||||
proxy_pass http://sac_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 120s;
|
||||
proxy_read_timeout 120s;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://sac_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env bash
|
||||
# Полное обновление SAC на сервере (native).
|
||||
# Установка: sudo cp /opt/security-alert-center/deploy/sac-deploy.sh /opt/sac-deploy.sh
|
||||
# sudo chmod 755 /opt/sac-deploy.sh
|
||||
# Запуск: sudo /opt/sac-deploy.sh
|
||||
#
|
||||
# Выполняет от пользователя sac: git pull, pip, alembic, npm build.
|
||||
# От root: systemctl restart sac-api.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
APP_ROOT="${SAC_APP_ROOT:-/opt/security-alert-center}"
|
||||
APP_USER="${SAC_APP_USER:-sac}"
|
||||
SERVICE_NAME="${SAC_SERVICE:-sac-api}"
|
||||
CONFIG_FILE="${SAC_CONFIG_FILE:-${APP_ROOT}/config/sac-api.env}"
|
||||
|
||||
log() { printf '[sac-deploy] %s\n' "$*"; }
|
||||
die() { printf '[sac-deploy] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
die "Запускайте от root: sudo /opt/sac-deploy.sh"
|
||||
fi
|
||||
|
||||
id "$APP_USER" &>/dev/null || die "Пользователь ${APP_USER} не существует"
|
||||
|
||||
if [ ! -d "${APP_ROOT}/.git" ]; then
|
||||
die "Не найден git-репозиторий: ${APP_ROOT}"
|
||||
fi
|
||||
|
||||
log "Выравнивание владельца ${APP_ROOT} → ${APP_USER}:${APP_USER}"
|
||||
chown -R "${APP_USER}:${APP_USER}" "${APP_ROOT}"
|
||||
|
||||
log "git fetch + sync"
|
||||
sudo -u "${APP_USER}" git -C "${APP_ROOT}" fetch --prune
|
||||
|
||||
if sudo -u "${APP_USER}" git -C "${APP_ROOT}" pull --ff-only 2>/dev/null; then
|
||||
log "git: fast-forward OK"
|
||||
else
|
||||
UPSTREAM="$(sudo -u "${APP_USER}" git -C "${APP_ROOT}" rev-parse --abbrev-ref '@{u}' 2>/dev/null || true)"
|
||||
if [ -z "${UPSTREAM}" ]; then
|
||||
UPSTREAM="origin/main"
|
||||
fi
|
||||
log "WARN: fast-forward невозможен (часто после force-push на kalinamall) — reset --hard ${UPSTREAM}"
|
||||
sudo -u "${APP_USER}" git -C "${APP_ROOT}" reset --hard "${UPSTREAM}"
|
||||
fi
|
||||
|
||||
if [ ! -f "${CONFIG_FILE}" ]; then
|
||||
die "Нет конфига: ${CONFIG_FILE}"
|
||||
fi
|
||||
|
||||
VENV="${APP_ROOT}/backend/.venv"
|
||||
if [ ! -x "${VENV}/bin/pip" ]; then
|
||||
die "Нет venv: ${VENV} — сначала установите по install-ubuntu-24.04-native.md"
|
||||
fi
|
||||
|
||||
log "pip install -r requirements.txt"
|
||||
sudo -u "${APP_USER}" "${VENV}/bin/pip" install -q -r "${APP_ROOT}/backend/requirements.txt"
|
||||
|
||||
log "Проверка ${CONFIG_FILE} (pydantic; без bash source — inline-комментарии в значениях ломают deploy)"
|
||||
sudo -u "${APP_USER}" bash -c "
|
||||
export SAC_CONFIG_FILE='${CONFIG_FILE}'
|
||||
cd '${APP_ROOT}/backend'
|
||||
'${VENV}/bin/python' -c 'from app.config import get_settings; get_settings(); print(\"config: OK\")'
|
||||
" || die "Неверный ${CONFIG_FILE}: одна переменная = одна строка, комментарии только отдельной строкой с # (не «false ← …» после значения)"
|
||||
|
||||
log "alembic upgrade head"
|
||||
sudo -u "${APP_USER}" bash -c "
|
||||
export SAC_CONFIG_FILE='${CONFIG_FILE}'
|
||||
cd '${APP_ROOT}/backend' && '${VENV}/bin/alembic' upgrade head
|
||||
"
|
||||
|
||||
if [ -f "${APP_ROOT}/frontend/package.json" ]; then
|
||||
if ! command -v npm >/dev/null 2>&1; then
|
||||
log "npm не найден — пропуск сборки UI (apt install nodejs npm)"
|
||||
else
|
||||
log "frontend: npm ci / build"
|
||||
sudo -u "${APP_USER}" bash -c "
|
||||
cd '${APP_ROOT}/frontend'
|
||||
if [ -f package-lock.json ]; then npm ci; else npm install; fi
|
||||
npm run build
|
||||
"
|
||||
if [ ! -f "${APP_ROOT}/frontend/dist/index.html" ]; then
|
||||
die "Сборка UI не создала frontend/dist/index.html"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "${APP_ROOT}/deploy/systemd/${SERVICE_NAME}.service" ]; then
|
||||
SYSTEMD_DST="/etc/systemd/system/${SERVICE_NAME}.service"
|
||||
if [ ! -f "${SYSTEMD_DST}" ] || ! cmp -s "${APP_ROOT}/deploy/systemd/${SERVICE_NAME}.service" "${SYSTEMD_DST}"; then
|
||||
log "Обновление systemd unit → ${SYSTEMD_DST}"
|
||||
cp "${APP_ROOT}/deploy/systemd/${SERVICE_NAME}.service" "${SYSTEMD_DST}"
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
fi
|
||||
START_SH="${APP_ROOT}/deploy/systemd/sac-api-start.sh"
|
||||
if [ -f "${START_SH}" ]; then
|
||||
sed -i 's/\r$//' "${START_SH}"
|
||||
chmod 755 "${START_SH}"
|
||||
fi
|
||||
for _sh in "${APP_ROOT}"/deploy/sac-deploy.sh "${APP_ROOT}"/deploy/systemd/*.sh; do
|
||||
[ -f "${_sh}" ] || continue
|
||||
sed -i 's/\r$//' "${_sh}"
|
||||
chmod 755 "${_sh}" 2>/dev/null || true
|
||||
done
|
||||
|
||||
log "Проверка DATABASE_URL (как у uvicorn через SAC_CONFIG_FILE)"
|
||||
sudo -u "${APP_USER}" bash -c "
|
||||
export SAC_CONFIG_FILE='${CONFIG_FILE}'
|
||||
cd '${APP_ROOT}/backend'
|
||||
'${VENV}/bin/python' -c \"
|
||||
from sqlalchemy import create_engine, text
|
||||
from app.config import get_settings
|
||||
engine = create_engine(get_settings().database_url, pool_pre_ping=True)
|
||||
with engine.connect() as conn:
|
||||
conn.execute(text('SELECT 1'))
|
||||
print('db: OK')
|
||||
\"
|
||||
" || die "PostgreSQL: проверьте DATABASE_URL в ${CONFIG_FILE} и systemctl status postgresql"
|
||||
|
||||
log "systemctl restart ${SERVICE_NAME} (краткий 502 в UI возможен ~10 с)"
|
||||
log "Сброс зависших remote_action (running без worker после restart)"
|
||||
sudo -u "${APP_USER}" bash -c "
|
||||
export SAC_CONFIG_FILE='${CONFIG_FILE}'
|
||||
cd '${APP_ROOT}/backend'
|
||||
'${VENV}/bin/python' -c \"
|
||||
from app.database import SessionLocal
|
||||
from app.services.host_remote_actions import clear_stale_running_remote_actions
|
||||
session = SessionLocal()
|
||||
try:
|
||||
names = clear_stale_running_remote_actions(session)
|
||||
for name in names:
|
||||
print(f'stale remote_action reset: {name}')
|
||||
finally:
|
||||
session.close()
|
||||
\"
|
||||
"
|
||||
systemctl restart "${SERVICE_NAME}"
|
||||
sleep 2
|
||||
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
|
||||
if systemctl is-active --quiet "${SERVICE_NAME}"; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
systemctl is-active --quiet "${SERVICE_NAME}" || die "${SERVICE_NAME} не active — journalctl -u ${SERVICE_NAME} -n 40 --no-pager"
|
||||
|
||||
HEALTH_OK=0
|
||||
for _ in 1 2 3 4 5 6; do
|
||||
sleep 2
|
||||
if curl -sf "http://127.0.0.1:8000/health" >/dev/null 2>&1; then
|
||||
HEALTH_OK=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "${HEALTH_OK}" -eq 1 ]; then
|
||||
log "health: OK (http://127.0.0.1:8000/health)"
|
||||
else
|
||||
die "${SERVICE_NAME} запущен, но /health не ответил — journalctl -u ${SERVICE_NAME} -n 80 --no-pager"
|
||||
fi
|
||||
|
||||
RENDER_DAILY_TIMER="${APP_ROOT}/deploy/systemd/render-sac-daily-report-timer.sh"
|
||||
if [ -f "${RENDER_DAILY_TIMER}" ]; then
|
||||
chmod +x "${RENDER_DAILY_TIMER}" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
for aux in sac-retention sac-daily-report; do
|
||||
for suffix in service timer; do
|
||||
src="${APP_ROOT}/deploy/systemd/${aux}.${suffix}"
|
||||
dst="/etc/systemd/system/${aux}.${suffix}"
|
||||
if [ "${aux}" = "sac-daily-report" ] && [ "${suffix}" = "timer" ]; then
|
||||
if [ -x "${RENDER_DAILY_TIMER}" ]; then
|
||||
log "Генерация ${dst} из ${CONFIG_FILE} (SAC_DAILY_REPORT_HOUR/TIMEZONE)"
|
||||
bash "${RENDER_DAILY_TIMER}" "${CONFIG_FILE}" "${dst}"
|
||||
systemctl daemon-reload
|
||||
systemctl restart "${aux}.timer" 2>/dev/null || systemctl start "${aux}.timer" 2>/dev/null || true
|
||||
elif [ -f "${src}" ]; then
|
||||
log "WARN: ${RENDER_DAILY_TIMER} не найден — копируем статический timer"
|
||||
cp "${src}" "${dst}"
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if [ -f "${src}" ]; then
|
||||
if [ ! -f "${dst}" ] || ! cmp -s "${src}" "${dst}"; then
|
||||
log "Обновление ${dst}"
|
||||
cp "${src}" "${dst}"
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [ -f "/etc/systemd/system/${aux}.timer" ]; then
|
||||
systemctl enable "${aux}.timer" 2>/dev/null || true
|
||||
systemctl start "${aux}.timer" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
log "Готово."
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# Генерирует sac-daily-report.timer из SAC_DAILY_REPORT_HOUR и SAC_DAILY_REPORT_TIMEZONE.
|
||||
# Usage: sudo deploy/systemd/render-sac-daily-report-timer.sh [config.env] [output.timer]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CONFIG_FILE="${1:-/opt/security-alert-center/config/sac-api.env}"
|
||||
OUTPUT="${2:-/etc/systemd/system/sac-daily-report.timer}"
|
||||
|
||||
HOUR=9
|
||||
TZ_NAME="Europe/Moscow"
|
||||
|
||||
if [ -f "${CONFIG_FILE}" ]; then
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
source "${CONFIG_FILE}"
|
||||
set +a
|
||||
fi
|
||||
|
||||
HOUR="${SAC_DAILY_REPORT_HOUR:-9}"
|
||||
TZ_NAME="${SAC_DAILY_REPORT_TIMEZONE:-Europe/Moscow}"
|
||||
|
||||
if ! [[ "${HOUR}" =~ ^[0-9]+$ ]] || [ "${HOUR}" -lt 0 ] || [ "${HOUR}" -gt 23 ]; then
|
||||
printf 'render-sac-daily-report-timer: invalid SAC_DAILY_REPORT_HOUR=%s\n' "${HOUR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
HOUR_PADDED="$(printf '%02d' "${HOUR}")"
|
||||
|
||||
cat >"${OUTPUT}" <<EOF
|
||||
[Unit]
|
||||
Description=Daily SAC report aggregation and notify
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* ${HOUR_PADDED}:00:00
|
||||
Timezone=${TZ_NAME}
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
|
||||
printf 'render-sac-daily-report-timer: wrote %s (OnCalendar=%s:00:00 Timezone=%s)\n' \
|
||||
"${OUTPUT}" "${HOUR_PADDED}" "${TZ_NAME}"
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# Uvicorn launcher: SAC_UVICORN_WORKERS из sac-api.env (без systemd EnvironmentFile).
|
||||
set -euo pipefail
|
||||
|
||||
APP_ROOT="${SAC_APP_ROOT:-/opt/security-alert-center}"
|
||||
CONFIG_FILE="${SAC_CONFIG_FILE:-${APP_ROOT}/config/sac-api.env}"
|
||||
VENV="${APP_ROOT}/backend/.venv"
|
||||
HOST="127.0.0.1"
|
||||
PORT="8000"
|
||||
WORKERS=4
|
||||
|
||||
_read_env_int() {
|
||||
local key="$1" default="$2" line val
|
||||
[ -f "$CONFIG_FILE" ] || {
|
||||
printf '%s\n' "$default"
|
||||
return 0
|
||||
}
|
||||
line="$(grep -E "^[[:space:]]*${key}=" "$CONFIG_FILE" 2>/dev/null | tail -1)" || {
|
||||
printf '%s\n' "$default"
|
||||
return 0
|
||||
}
|
||||
val="${line#*=}"
|
||||
val="${val#"${val%%[![:space:]]*}"}"
|
||||
val="${val%"${val##*[![:space:]]}"}"
|
||||
val="${val#\"}"
|
||||
val="${val%\"}"
|
||||
val="${val#\'}"
|
||||
val="${val%\'}"
|
||||
if [[ "$val" =~ ^[0-9]+$ ]] && [ "$val" -ge 1 ]; then
|
||||
printf '%s\n' "$val"
|
||||
else
|
||||
printf '%s\n' "$default"
|
||||
fi
|
||||
}
|
||||
|
||||
WORKERS="$(_read_env_int SAC_UVICORN_WORKERS 4)"
|
||||
|
||||
exec "${VENV}/bin/uvicorn" app.main:app \
|
||||
--host "$HOST" \
|
||||
--port "$PORT" \
|
||||
--workers "$WORKERS" \
|
||||
--timeout-graceful-shutdown 30
|
||||
@@ -0,0 +1,24 @@
|
||||
[Unit]
|
||||
Description=Security Alert Center API (FastAPI)
|
||||
Documentation=https://git.papatramp.ru/PapaTramp/security-alert-center
|
||||
After=network-online.target postgresql.service
|
||||
Wants=network-online.target
|
||||
Requires=postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=sac
|
||||
Group=sac
|
||||
WorkingDirectory=/opt/security-alert-center/backend
|
||||
# Конфиг читает само приложение (pydantic), не systemd — иначе ломаются пароли с #, $ и т.д.
|
||||
Environment=SAC_CONFIG_FILE=/opt/security-alert-center/config/sac-api.env
|
||||
Environment=PYTHONPATH=/opt/security-alert-center/backend
|
||||
ExecStart=/usr/bin/bash /opt/security-alert-center/deploy/systemd/sac-api-start.sh
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
TimeoutStopSec=20
|
||||
TimeoutStartSec=30
|
||||
NoNewPrivileges=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=Security Alert Center daily report generation (F-NOT-05)
|
||||
After=network-online.target postgresql.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=sac
|
||||
Group=sac
|
||||
WorkingDirectory=/opt/security-alert-center/backend
|
||||
EnvironmentFile=/opt/security-alert-center/config/sac-api.env
|
||||
ExecStart=/opt/security-alert-center/backend/.venv/bin/python -m app.jobs.daily_report
|
||||
Nice=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Daily SAC report aggregation and notify
|
||||
|
||||
[Timer]
|
||||
# Шаблон по умолчанию (hour=9, Europe/Moscow). На prod timer перегенерируется
|
||||
# из config/sac-api.env: deploy/systemd/render-sac-daily-report-timer.sh или sac-deploy.sh
|
||||
OnCalendar=*-*-* 09:00:00
|
||||
Timezone=Europe/Moscow
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=Security Alert Center host silence scan (stale heartbeat)
|
||||
After=network-online.target postgresql.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=sac
|
||||
Group=sac
|
||||
WorkingDirectory=/opt/security-alert-center/backend
|
||||
EnvironmentFile=/opt/security-alert-center/config/sac-api.env
|
||||
ExecStart=/opt/security-alert-center/backend/.venv/bin/python -m app.jobs.host_silence_scan
|
||||
Nice=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Periodic SAC host silence scan (stale agent.heartbeat)
|
||||
|
||||
[Timer]
|
||||
OnBootSec=3min
|
||||
OnUnitActiveSec=5min
|
||||
RandomizedDelaySec=30
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=Security Alert Center data retention
|
||||
After=network-online.target postgresql.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=sac
|
||||
Group=sac
|
||||
WorkingDirectory=/opt/security-alert-center/backend
|
||||
EnvironmentFile=/opt/security-alert-center/config/sac-api.env
|
||||
ExecStart=/opt/security-alert-center/backend/.venv/bin/python -m app.jobs.retention
|
||||
Nice=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Daily SAC retention purge
|
||||
|
||||
[Timer]
|
||||
OnCalendar=daily
|
||||
RandomizedDelaySec=900
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
Reference in New Issue
Block a user