diff --git a/deploy/ish/configure-ish-v107.sql b/deploy/ish/configure-ish-v107.sql new file mode 100644 index 0000000..a7f71bb --- /dev/null +++ b/deploy/ish/configure-ish-v107.sql @@ -0,0 +1,69 @@ +-- configure-ish-v107.sql — параметры канала WSL для ИШ версии 107 (TEST3 GOST). +-- Применять ПОСЛЕ обновления ИШ до 107 (миграция для TEST3 их НЕ переносит — +-- см. obnovlenie107.pdf, «Для тестового контура TEST3 миграция параметров канала +-- WSL не предусмотрена»). +-- +-- Что делает: +-- 1. wslEndpoint → КОРОТКИЙ базовый URL (v107 сам добавляет /api/v1/api-key/make; +-- старый v100-путь .../OnyxEdoWSService/OnyxEdo ломает JWT api-key). +-- 2. Новые TLS-параметры канала: отпечаток УЦ (TEST3 GOST!), DN, длина цепочки. +-- 3. Флаг M2M (ускорение переводов по Указанию ЦБ 7317-У). +-- 4. Очищает нерасшифровываемые v100-пароли Web API (при auth off + http). +-- +-- Использование: +-- sudo -u postgres psql -d igate -v chan_id=33 -f configure-ish-v107.sql +-- где chan_id — id строки канала в таблице channels (обычно 33; проверить: +-- sudo -u postgres psql -d igate -c "select id,code from channels"). +-- +-- ⚠️ Значения ниже — для контура TEST3 GOST. Для других контуров/крипты возьмите +-- URL и отпечаток из таблиц «Адреса контуров» и «Отпечатки сертификатов» +-- obnovlenie107.pdf. НЕ перепутать GOST и RSA (у нас канал GOST). + +\set chan :chan_id + +BEGIN; + +-- 1. Короткий базовый URL канала (v107). TEST3 GOST. +UPDATE parameters SET value = 'https://gost-t3.nsd.ru/onyx-ms' + WHERE name = 'wslEndpoint' AND chanel_id = :chan; + +-- 2. Новые TLS-параметры канала v107 (INSERT если нет, UPDATE если есть). +-- Отпечаток УЦ МБ — TEST3 GOST (НЕ RSA!): +-- EB:C2:D6:CA:73:AC:22:69:44:09:07:ED:6A:58:2C:4E:55:29:5D:00:F8:0F:1D:1E:70:A0:85:25:32:0D:DC:BF +INSERT INTO parameters(name, value, chanel_id) VALUES + ('wslTlsCACertFingerprint', + 'EB:C2:D6:CA:73:AC:22:69:44:09:07:ED:6A:58:2C:4E:55:29:5D:00:F8:0F:1D:1E:70:A0:85:25:32:0D:DC:BF', + :chan), + ('wslTlsCertDN', '*.nsd.ru', :chan), + ('wslTlsCertChainLen', '2', :chan), + ('isM2M', 'True', :chan) +ON CONFLICT (name, chanel_id) DO UPDATE SET value = EXCLUDED.value; + +-- 2b. Флаги ускорения M2M (Указание ЦБ 7317-У, соблюдение регламентных сроков): +-- интервал выгрузки сообщений 10 с (было 1 мин) и игнорирование вложенности +-- папок при обработке входящих. См. презентацию НРД «новые версии ИШ». +-- «Время ожидания перед отправкой» = monitoringThreshold (как долго ИШ ждёт +-- добавления файлов в папку перед отправкой, HH:MM:SS, мин 00:00:01) → +-- ставим минимум для быстрого M2M. «Интервал выгрузки сообщений» = +-- fetchInterval (обращение к ONYX) → 00:00:10. +-- Флаг «Автоотправка уведомлений о приёме транзитного сообщения» к нашему +-- каналу НЕ применяется (канал не транзитный, isTransitTerminalChannel=False). +UPDATE parameters SET value='00:00:10' WHERE name='fetchInterval' AND chanel_id=:chan; +UPDATE parameters SET value='00:00:01' WHERE name='monitoringThreshold' AND chanel_id=:chan; +UPDATE parameters SET value='True' WHERE name='ignorePackageDirectoryStructure' AND chanel_id=:chan; +UPDATE parameters SET value='ERRORS' WHERE name='errorDirName' AND chanel_id=:chan; + +-- 3. Очистка v100-зашифрованных паролей Web API, которые v107 не расшифровывает. +-- Безопасно при server.authentication.enable=False и server.scheme=Http +-- (проверьте эти значения перед применением). +UPDATE parameters SET value = '' + WHERE name IN ('server.authentication.password', 'server.certificate.file.password'); + +COMMIT; + +-- Контроль: показать итоговые значения. +\echo '--- итоговые параметры канала v107 ---' +SELECT name, value FROM parameters + WHERE (name LIKE 'wsl%' OR name IN ('isM2M','fetchInterval','ignorePackageDirectoryStructure')) + AND chanel_id = :chan + ORDER BY name; diff --git a/deploy/ish/upgrade-ish-v107.sh b/deploy/ish/upgrade-ish-v107.sh new file mode 100755 index 0000000..72c740f --- /dev/null +++ b/deploy/ish/upgrade-ish-v107.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env bash +# upgrade-ish-v107.sh — полное автоматическое обновление ИШ до версии 107 на +# Astra Linux (amd64) в headless-режиме (сервер без графической сессии). +# +# ИШ 107 принёс изменения, из-за которых он «из коробки» не стартует как +# systemd-служба без графики. Этот скрипт решает ВСЕ слои (проверено 19.08.2026, +# см. память ish-v107-upgrade): +# 1. Установка .deb (dpkg). +# 2. gnome-keyring для секретов (v107 хранит пароли в OS SecretStore/libsecret). +# 3. Виртуальный дисплей Xvfb (v107 igate-svc тянет GTK/GVFS). +# 4. Параметры канала WSL для v107 (короткий URL, TLS-отпечаток GOST, DN, chain, +# M2M) — через configure-ish-v107.sql. +# 5. Самоочистка stale-состояния (ExecStartPre) — иначе рестарты флакуют. +# +# Конфигурация ИШ живёт в PostgreSQL и ПЕРЕЖИВАЕТ обновление. +# +# Использование: +# sudo ./upgrade-ish-v107.sh /путь/к/igate-107-0-930-amd64.zip +# Опционально переменные окружения: +# SVC_USER (по умолчанию igate) — пользователь службы ИШ +# IGATE_DB (по умолчанию igate) — имя БД PostgreSQL +# CHAN_ID (по умолчанию auto) — id канала в channels (auto = найти WSL) +# KEYRING_PASS_FILE (/var/lib/igate/.keyring-pass) — файл пароля keyring +# +set -euo pipefail + +ZIP="${1:?укажите путь к igate-107-*-amd64.zip}" +SVC_USER="${SVC_USER:-igate}" +IGATE_DB="${IGATE_DB:-igate}" +IGATE_DIR="/opt/nsd/igate" +WRAPPER="/usr/local/bin/igate-run.sh" # вне /opt — переживает переустановку .deb +DROPIN_DIR="/etc/systemd/system/igate.service.d" +SQL_DIR="$(cd "$(dirname "$0")" && pwd)" +STAMP="$(date +%Y%m%d-%H%M%S)" +WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT + +log(){ echo -e "\n=== $* ==="; } +[ "$(id -u)" = 0 ] || { echo "нужен root (sudo)"; exit 1; } + +SVC_UID="$(id -u "$SVC_USER")" +KEYRING_PASS_FILE="${KEYRING_PASS_FILE:-/var/lib/$SVC_USER/.keyring-pass}" +HOME_DIR="$(getent passwd "$SVC_USER" | cut -d: -f6)" + +log "0/9 Проверка зависимостей" +for bin in dpkg gnome-keyring-daemon dbus-run-session xvfb-run loginctl psql; do + command -v "$bin" >/dev/null || { echo "нет $bin — установите (gnome-keyring, dbus-x11, xvfb, libsecret-tools, postgresql-client)"; exit 1; } +done +echo "все зависимости на месте" + +log "1/9 Распаковка дистрибутива" +unzip -o -q "$ZIP" -d "$WORK" +DEB="$(ls "$WORK"/*.deb 2>/dev/null | head -1)" +[ -n "$DEB" ] || { echo "в архиве нет .deb"; exit 1; } +echo "пакет: $(basename "$DEB") версия: $(dpkg-deb -f "$DEB" Version)" + +log "2/9 Бэкап установки, конфига службы и БД" +mkdir -p "/var/backups/ish-pre107-$STAMP" +BK="/var/backups/ish-pre107-$STAMP" +[ -d "$IGATE_DIR" ] && tar czf "$BK/opt-nsd-igate.tar.gz" -C "$(dirname "$IGATE_DIR")" "$(basename "$IGATE_DIR")" 2>/dev/null || true +[ -d "$DROPIN_DIR" ] && cp -a "$DROPIN_DIR" "$BK/dropin" || true +( cd /tmp && sudo -u postgres pg_dump -d "$IGATE_DB" -Fc | cat > "$BK/igate-db.dump" ) 2>/dev/null || echo " (пропуск дампа БД)" +echo "$BK" > /var/backups/ish-last-backup.txt +echo "бэкап: $BK" + +log "3/9 Остановка службы и очистка stale-состояния" +systemctl stop igate 2>/dev/null || true +pkill -9 -u "$SVC_USER" -f igate-svc 2>/dev/null || true +pkill -9 -u "$SVC_USER" -f Xvfb 2>/dev/null || true +rm -f /tmp/CoreFxPipe_NSD.IGate.* 2>/dev/null || true + +log "4/9 Установка пакета (.deb)" +echo "igate igate/service boolean false" | debconf-set-selections +echo "igate igate/shortcut boolean false" | debconf-set-selections +DEBIAN_FRONTEND=noninteractive dpkg -i --force-confnew "$DEB" || apt-get -f install -y || true +echo "версия установлена: $(python3 -c "import json;print([k for k in json.load(open('$IGATE_DIR/igate/igate.deps.json'))['libraries'] if k.lower().startswith('igate/')][0])" 2>/dev/null)" + +log "5/9 Подготовка keyring (пароль + linger для XDG_RUNTIME_DIR)" +loginctl enable-linger "$SVC_USER" +if [ ! -f "$KEYRING_PASS_FILE" ]; then + umask 077 + # локальный пароль keyring (только для сервисного хранилища секретов ИШ) + head -c 18 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 20 > "$KEYRING_PASS_FILE" + chown "$SVC_USER":"$SVC_USER" "$KEYRING_PASS_FILE"; chmod 600 "$KEYRING_PASS_FILE" + echo "создан $KEYRING_PASS_FILE" +else + echo "используется существующий $KEYRING_PASS_FILE" +fi +# сбросить возможно несовместимый старый login.keyring — wrapper создаст заново +rm -f "$HOME_DIR/.local/share/keyrings/login.keyring" 2>/dev/null || true + +log "6/9 Установка wrapper $WRAPPER" +cat > "$WRAPPER" </dev/null)" +export GNOME_KEYRING_CONTROL SSH_AUTH_SOCK +cd "\$HOME" +exec xvfb-run -a $IGATE_DIR/igate-svc/igate-svc +EOF +chmod +x "$WRAPPER"; echo "wrapper установлен" + +log "7/9 Настройка systemd (drop-in с самоочисткой и авто-рестартом)" +mkdir -p "$DROPIN_DIR" +cat > "$DROPIN_DIR/override.conf" </dev/null; true' +ExecStart= +ExecStart=/usr/bin/dbus-run-session -- $WRAPPER +Restart=on-failure +RestartSec=8 +EOF +systemctl daemon-reload + +log "8/9 Параметры канала v107 (SQL) + миграция паролей" +CHAN_ID="${CHAN_ID:-auto}" +if [ "$CHAN_ID" = auto ]; then + CHAN_ID="$(cd /tmp && sudo -u postgres psql -d "$IGATE_DB" -tAc "select id from channels where type='WSL' order by id limit 1" 2>/dev/null | tr -d '[:space:]')" +fi +[ -n "$CHAN_ID" ] || { echo "не найден WSL-канал в channels — укажите CHAN_ID вручную"; exit 1; } +echo "канал id=$CHAN_ID" +if [ -f "$SQL_DIR/configure-ish-v107.sql" ]; then + ( cd /tmp && sudo -u postgres psql -d "$IGATE_DB" -v chan_id="$CHAN_ID" -f "$SQL_DIR/configure-ish-v107.sql" ) +else + echo " ⚠️ configure-ish-v107.sql не найден рядом со скриптом — параметры канала НЕ применены" +fi +# Утилита миграции v107 (флаг миграции); финальный Console.ReadKey в headless +# безвреден — миграция сохраняется до него. +if [ -x "$IGATE_DIR/igate-mig/igate-mig" ]; then + sudo -u "$SVC_USER" bash -c ' + export HOME='"$HOME_DIR"' XDG_DATA_HOME='"$HOME_DIR"'/.local/share XDG_RUNTIME_DIR=/run/user/'"$SVC_UID"' + dbus-run-session -- bash -c " + eval \$(printf %s\\\\n \"\$(cat '"$KEYRING_PASS_FILE"')\" | gnome-keyring-daemon --replace --unlock --components=secrets,pkcs11 2>/dev/null) + echo \"\" | timeout 30 '"$IGATE_DIR"'/igate-mig/igate-mig --data '"$HOME_DIR"' >/dev/null 2>&1 || true + "' 2>/dev/null || true + echo "igate-mig выполнен (флаг миграции)" +fi + +log "9/9 Запуск и проверка" +systemctl start igate +OK=0 +for i in $(seq 1 8); do + sleep 5 + if ss -tlnp 2>/dev/null | grep -q ':8090'; then OK=1; break; fi + systemctl is-active --quiet igate || break +done +VER="$(python3 -c "import json;print([k for k in json.load(open('$IGATE_DIR/igate/igate.deps.json'))['libraries'] if k.lower().startswith('igate/')][0])" 2>/dev/null || true)" +STATE="$(curl -s http://localhost:8090/api/admin/engine/state 2>/dev/null || true)" +echo "версия: $VER" +echo "engine/state: ${STATE:-нет ответа}" +echo "8090: $(ss -tlnp 2>/dev/null | grep -c ':8090') слушатель" +if [ "$OK" = 1 ] && [ "$STATE" = '"Running"' ]; then + echo -e "\n✅ ИШ 107 запущен и работает." +else + cat < $DROPIN_DIR/override.conf && + systemctl daemon-reload && systemctl start igate +MSG +fi diff --git a/deploy/ish/upgrade-ish.sh b/deploy/ish/upgrade-ish.sh new file mode 100755 index 0000000..683b4ea --- /dev/null +++ b/deploy/ish/upgrade-ish.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# upgrade-ish.sh — автоматическое обновление ПО «Интеграционный шлюз НРД» (igate) +# на Astra Linux (amd64). Конфигурация ИШ живёт в PostgreSQL и ПЕРЕЖИВАЕТ +# обновление — пакет заменяет только бинарники в /opt/nsd/igate. +# +# Дистрибутив — .deb с сайта НРД (стабильный URL, без авторизации): +# https://www.nsd.ru/media/integratsionnyj-shlyuz//igate--amd64.zip +# (внутри igate__amd64.deb) + <...>-amd64sgn.zip (отделённая подпись). +# +# ⚠️ TEST3: миграция параметров канала WSL утилитой igate-mig НЕ предусмотрена +# (см. obnovlenie107.pdf). Параметры канала WSL для TEST3 обновляются вручную +# или нашим configure-ish.sql. Пароли БД у нас через trust — миграция паролей +# в защищённое хранилище ОС не обязательна. +# +# Использование: +# sudo ./upgrade-ish.sh /путь/к/igate-107-0-930-amd64.zip [/путь/к/...sgn.zip] +# +set -euo pipefail + +ZIP="${1:?укажите путь к igate-*-amd64.zip}" +SGN="${2:-}" +SVC_USER="igate" +IGATE_DIR="/opt/nsd/igate" +STAMP="$(date +%Y%m%d-%H%M%S)" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +log(){ echo -e "\n=== $* ==="; } + +[ "$(id -u)" = 0 ] || { echo "нужен root (sudo)"; exit 1; } + +log "1/8 Распаковка дистрибутива" +unzip -o -q "$ZIP" -d "$WORK" +DEB="$(ls "$WORK"/*.deb 2>/dev/null | head -1)" +[ -n "$DEB" ] || { echo "в архиве нет .deb"; exit 1; } +echo "пакет: $(basename "$DEB") версия: $(dpkg-deb -f "$DEB" Version)" + +if [ -n "$SGN" ]; then + log "2/8 Подпись дистрибутива приложена: $(basename "$SGN")" + echo "(проверку подписи выполняет средство НРД/Валидата — файл сохранён рядом)" +else + log "2/8 Файл подписи не передан — пропуск" +fi + +log "3/8 Бэкап текущей установки и конфига сервиса" +if [ -d "$IGATE_DIR" ]; then + tar czf "/var/backups/igate-$STAMP.tar.gz" -C "$(dirname "$IGATE_DIR")" "$(basename "$IGATE_DIR")" 2>/dev/null || true + echo "бэкап: /var/backups/igate-$STAMP.tar.gz" +fi +# Наш drop-in (WorkingDirectory=/var/lib/igate, User=igate) — сохраняем для восстановления. +DROPIN="/etc/systemd/system/igate.service.d" +[ -d "$DROPIN" ] && cp -a "$DROPIN" "$WORK/dropin-backup" && echo "drop-in сохранён" + +log "4/8 Остановка сервиса igate" +systemctl stop igate 2>/dev/null || true +# GUI и служба не должны работать одновременно с одной БД. +pkill -u "$SVC_USER" -f '/opt/nsd/igate/igate/igate' 2>/dev/null || true + +log "5/8 Preseed debconf (без интерактива) и установка пакета" +# postinst спрашивает igate/service (регистрация сервиса) и igate/shortcut. +echo "igate igate/service boolean true" | debconf-set-selections +echo "igate igate/shortcut boolean false" | debconf-set-selections +# postinst использует logname → в неинтерактиве может падать; задаём SUDO_USER. +export DEBIAN_FRONTEND=noninteractive +if ! dpkg -i "$DEB"; then + echo "dpkg сообщил об ошибке — пробуем довести зависимости" + apt-get -f install -y || true +fi + +log "6/8 Восстановление нашего drop-in и пользователя сервиса" +# Пакет мог перезаписать igate.service (User=, WD=/home). +mkdir -p "$DROPIN" +if [ -d "$WORK/dropin-backup" ]; then + cp -a "$WORK/dropin-backup/." "$DROPIN/" +else + cat > "$DROPIN/override.conf" </dev/null || true +systemctl daemon-reload + +log "7/8 Запуск сервиса и проверка" +systemctl start igate +sleep 6 +STATE="$(curl -s http://localhost:8090/api/admin/engine/state 2>/dev/null || true)" +VER="$(grep -oE '"igate/[0-9.]+"' "$IGATE_DIR/igate/igate.deps.json" 2>/dev/null | head -1)" +echo "engine/state: ${STATE:-нет ответа}" +echo "версия: ${VER:-неизвестно}" + +log "8/8 Готово" +cat <: +ignoreregex = diff --git a/deploy/linux/fail2ban/jail.d/bj.conf b/deploy/linux/fail2ban/jail.d/bj.conf new file mode 100644 index 0000000..a2ebc2b --- /dev/null +++ b/deploy/linux/fail2ban/jail.d/bj.conf @@ -0,0 +1,23 @@ +# Сервер bj: SSH и вход в веб-интерфейс bj (/etc/fail2ban/jail.d/bj.conf). +# bj пишет журнал в /var/log/bj/bj-server.err; сотрудники ошибаются с паролем AD, поэтому +# порог для bj мягче, чем для SSH. Если SSH пишет только в журнал systemd — backend = systemd +# вместо logpath. +[DEFAULT] +ignoreip = 127.0.0.1/8 ::1 +banaction = iptables-multiport + +[sshd] +enabled = true +maxretry = 5 +findtime = 600 +bantime = 3600 +logpath = /var/log/auth.log + +[bj-admin] +enabled = true +filter = bj-admin +logpath = /var/log/bj/bj-server.err +port = 8080,8099 +maxretry = 10 +findtime = 600 +bantime = 1800 diff --git a/deploy/linux/sbin/bj-keymedia-install b/deploy/linux/sbin/bj-keymedia-install new file mode 100755 index 0000000..f1dbf98 --- /dev/null +++ b/deploy/linux/sbin/bj-keymedia-install @@ -0,0 +1,208 @@ +#!/bin/bash +# bj-keymedia-install — установка ключа Валидаты на съёмный носитель. +# Сохраняет структуру архива НРД (key.reg + spr/ + vdkeys/) в профиль на +# носителе, ПЕРЕСОБИРАЕТ совместимый справочник сертификатов (архивный gdbm НРД +# несовместим — bj-validata-rebuild-gdbm чинит), и формирует профиль для ИШ. +# inspect +# install [profile_name] +# restart-ish +set -uo pipefail +DEFAULT_KEYMEDIA=/var/lib/igate/keymedia +BACKUP_DIR=/var/lib/bj/key-backups +IGATE_PROFILES=/var/lib/igate/profiles +IGATE_INI=/var/lib/igate/.Validata/zpki.ini +VDLIB=/opt/Validata/VDCSP/lib/amd64 +ZPKI=/opt/Validata/VDCSP/bin/amd64/zpki1utl +REBUILD=/usr/local/sbin/bj-validata-rebuild-gdbm +ACTION="${1:-}"; STAGING="${2:-}"; PROFILE="${3:-}" +TARGET_MNT="${BJ_TARGET_MNT:-}"; TARGET_DEV="${BJ_TARGET_DEV:-}" +err(){ echo "{\"ok\":false,\"error\":\"$1\"}"; exit 1; } + +if [ "$ACTION" = "restart-ish" ]; then + systemctl restart igate.service >/dev/null 2>&1 || systemctl restart igate-svc >/dev/null 2>&1 || true + echo '{"ok":true,"action":"restart-ish"}'; exit 0 +fi + +# ---- сертификаты НРД в справочнике активного профиля ИШ ---- +prof_dir(){ + PROFILE_NAME=$(su -s /bin/sh postgres -c "psql -d igate -tAc \"select value from parameters where name='cryptography.profile'\"" 2>/dev/null | tr -d '[:space:]') + [ -n "$PROFILE_NAME" ] || err "профиль ИШ не задан (cryptography.profile)" + PROFDIR="$IGATE_PROFILES/$PROFILE_NAME" + [ -f "$PROFDIR/local.gdbm" ] && [ -f "$PROFDIR/local.pse" ] || err "нет справочника в $PROFDIR" +} +certs_list_json(){ + python3 - "$PROFDIR/local.pse" "$PROFDIR/local.gdbm" <<'PY2' +import sys,subprocess,json,re +out=[]; seen=set() +for p in sys.argv[1:]: + store=p.rsplit('.',1)[-1] + try: d=open(p,'rb').read() + except Exception: continue + i=0 + while True: + i=d.find(b'\x30\x82',i) + if i<0: break + L=int.from_bytes(d[i+2:i+4],'big'); b=d[i:i+4+L] + if 100"$BK/rebuild.log") + echo "$OUT" | grep -q '"ok":true' || { restore; echo "$OUT"; exit 1; } + chown igate:igate "$PROFDIR"/local.*; chmod 644 "$PROFDIR"/local.* + # оракул: подписать файл этим профилем (без GUI). Провал → откат. + T=$(mktemp -d); echo test > "$T/f"; chown -R igate:igate "$T" + SIG=$(su -s /bin/sh igate -c "HOME=/var/lib/igate LD_LIBRARY_PATH=$VDLIB $ZPKI -sign -profile '$PROFILE_NAME' -registry -in '$T/f' -out '$T/s' -sendcert" 2>&1); RC=$? + SIGOK=0; [ $RC -eq 0 ] && [ -s "$T/s" ] && SIGOK=1; echo "$SIG" | grep -qiE 'ошибк|error' && SIGOK=0 + printf '%s\n' "$SIG" > "$BK/sign-check.log"; rm -rf "$T" + if [ $SIGOK -ne 1 ]; then restore; systemctl restart igate.service >/dev/null 2>&1; err "проверка подписи после пересборки не прошла — справочник откачен: $(echo "$SIG" | grep -iE 'ошибк|error|Результат' | head -1 | tr -d '"\\')"; fi + systemctl restart igate.service >/dev/null 2>&1 || systemctl restart igate-svc >/dev/null 2>&1 || true + sleep 5; ACT=$(systemctl is-active igate.service 2>/dev/null || echo unknown) + echo "{\"ok\":true,\"profile\":\"$PROFILE_NAME\",\"backup\":\"$BK\",\"igate\":\"$ACT\",\"rebuild\":$OUT,\"certs\":$(certs_list_json)}"; exit 0 +fi + +[ -n "$STAGING" ] && [ -d "$STAGING" ] || err "staging не найден" +case "$STAGING" in /var/lib/bj/media/iso/*) : ;; *) err "staging вне зоны" ;; esac +VDK=$(find "$STAGING" -iname '*.vdk' | head -1) +GDBM=$(find "$STAGING" -iname '*.gdbm' | head -1) +[ -n "$VDK" ] || err "в архиве нет .vdk" +VDK_NAME=$(basename "$VDK") +SPRDIR=""; [ -n "$GDBM" ] && SPRDIR=$(dirname "$GDBM") # .../spr1046 +SPRNAME=""; [ -n "$SPRDIR" ] && SPRNAME=$(basename "$SPRDIR") # spr1046 +USERNUM=$(echo "$SPRNAME" | grep -oE '[0-9]+' | head -1) # 1046 + +if [ "$ACTION" = "inspect" ]; then + printf '{"ok":true,"vdk":"%s","profile_dir":"%s","has_gdbm":%s,"has_crl":%s}\n' \ + "$VDK_NAME" "$SPRNAME" "$([ -n "$GDBM" ]&&echo true||echo false)" \ + "$([ -n "$(find "$STAGING" -iname '*.crl' | head -1)" ]&&echo true||echo false)" + exit 0 +fi +[ "$ACTION" = "install" ] || err "неизвестное действие" + +# ---- целевая точка монтирования (флешка) ---- +TMP_MNT=""; UMOUNT_AFTER=0 +[ -z "$TARGET_MNT" ] && [ -z "$TARGET_DEV" ] && TARGET_MNT="$DEFAULT_KEYMEDIA" +if [ -z "$TARGET_MNT" ] && [ -n "$TARGET_DEV" ]; then + TMP_MNT="/run/bj-keymedia-$$"; mkdir -p "$TMP_MNT" + mount "$TARGET_DEV" "$TMP_MNT" || err "не смонтировать $TARGET_DEV" + TARGET_MNT="$TMP_MNT"; UMOUNT_AFTER=1 +fi +[ -d "$TARGET_MNT" ] || err "целевой носитель недоступен" + +TS=$(date -u +%Y%m%d-%H%M%S); mkdir -p "$BACKUP_DIR" +BACKUP="$BACKUP_DIR/keymedia-$TS.tar.gz" +tar czf "$BACKUP" -C "$TARGET_MNT" . 2>/dev/null || err "бэкап не удался" + +REMOUNTED=0 +mount | grep -q " $TARGET_MNT .*[(,]ro[,)]" && { mount -o remount,rw "$TARGET_MNT" || err "remount rw не удался"; REMOUNTED=1; } +finish(){ sync; [ "$REMOUNTED" = 1 ] && mount -o remount,ro "$TARGET_MNT" 2>/dev/null; [ "$UMOUNT_AFTER" = 1 ] && { umount "$TMP_MNT" 2>/dev/null; rmdir "$TMP_MNT" 2>/dev/null; }; } +trap finish EXIT + +# имя профиля: задано пользователем, иначе PrUser +[ -n "$PROFILE" ] || PROFILE="PrUser${USERNUM:-_$TS}" +case "$PROFILE" in *[!A-Za-z0-9_-]*) err "недопустимое имя профиля" ;; esac +DEST="$TARGET_MNT/$PROFILE" +rm -rf "$DEST"; mkdir -p "$DEST" "$TARGET_MNT/vdkeys" +cp -rf "$STAGING"/. "$DEST"/ 2>/dev/null # key.reg + spr/ + vdkeys/ +cp -f "$VDK" "$TARGET_MNT/vdkeys/$VDK_NAME" +sync + +# ---- зарегистрировать ключевой носитель (vdmkdev читает ключ с флешки) ---- +systemctl restart vdmkdev.service >/dev/null 2>&1 || /etc/init.d/vdmkdev restart >/dev/null 2>&1 || true +sleep 2 + +# ---- ПЕРЕСОБРАТЬ совместимый справочник на флешке (архивный gdbm malformed) ---- +REBUILD_RES="skip"; FLASH_SPR="$DEST/$SPRNAME" +if [ -n "$SPRNAME" ] && [ -x "$REBUILD" ] && [ -f "$FLASH_SPR/local.pse" ]; then + RB=$("$REBUILD" "$FLASH_SPR" 2>&1) + if echo "$RB" | grep -q '"ok":true'; then REBUILD_RES="ok"; else REBUILD_RES="fail: $(echo "$RB"|tr -d '\n"'|head -c160)"; fi + sync +fi + +# ---- сформировать профиль для ИШ (стораны на диске, ключ остаётся на флешке) ---- +IGATE_RES="skip" +if [ "$REBUILD_RES" = "ok" ] && [ -d "$(dirname "$IGATE_PROFILES")" ]; then + IGP="$IGATE_PROFILES/$PROFILE" + rm -rf "$IGP"; mkdir -p "$IGP" + cp -f "$FLASH_SPR/local.pse" "$FLASH_SPR/local.gdbm" "$IGP"/ 2>/dev/null + # CRL-файлы рядом (если есть) — для офлайн-валидации + find "$DEST" -iname '*.crl' -exec cp -f {} "$IGP"/ \; 2>/dev/null + chown -R igate:igate "$IGP" 2>/dev/null + chmod 644 "$IGP"/local.* 2>/dev/null + # зарегистрировать профиль в zpki.ini ИШ (идемпотентно) + if [ -f "$IGATE_INI" ]; then + python3 - "$IGATE_INI" "$PROFILE" "$IGP" <<'PY' && IGATE_RES="ok" || IGATE_RES="ini-fail" +import sys +ini,name,igp=sys.argv[1],sys.argv[2],sys.argv[3] +L=open(ini).read().split("\n") +# убрать прежний блок профиля с тем же именем +out=[]; skip=False +i=0 +while i/dev/null 2>&1; then + IGATE_RES="connected" + fi + fi +fi + +finish; trap - EXIT +printf '{"ok":true,"profile":"%s","spr":"%s","vdk":"%s","backup":"%s","target":"%s","rebuild":"%s","igate":"%s","igate_profile_dir":"%s"}\n' \ + "$PROFILE" "$SPRNAME" "$VDK_NAME" "$BACKUP" "$TARGET_MNT" "$REBUILD_RES" "$IGATE_RES" "$IGATE_PROFILES/$PROFILE" diff --git a/deploy/linux/sbin/bj-keymedia-worker b/deploy/linux/sbin/bj-keymedia-worker new file mode 100755 index 0000000..003e9ee --- /dev/null +++ b/deploy/linux/sbin/bj-keymedia-worker @@ -0,0 +1,22 @@ +#!/bin/bash +# bj-keymedia-worker — демон обработки запросов установки ключа (host namespace). +REQDIR=/var/lib/bj/keymedia-requests +getf(){ python3 -c "import json;print(json.load(open('$1')).get('$2',''))" 2>/dev/null; } +process_one(){ + local req="$1" id res action staging profile tdev tmnt extra out + id=$(basename "$req" .req); res="$REQDIR/$id.res" + action=$(getf "$req" action); staging=$(getf "$req" staging); profile=$(getf "$req" profile) + tdev=$(getf "$req" target_dev); tmnt=$(getf "$req" target_mnt); extra=$(getf "$req" extra) + rm -f "$req" + case "$action" in + quikexport-*) out=$(BJ_EXTRA="$extra" /usr/local/sbin/bj-quikexport-admin "${action#quikexport-}" "$staging" 2>&1) ;; + *) out=$(BJ_TARGET_DEV="$tdev" BJ_TARGET_MNT="$tmnt" BJ_EXTRA="$extra" /usr/local/sbin/bj-keymedia-install "$action" "$staging" "$profile" 2>&1) ;; + esac + printf '%s' "$out" > "$res.tmp"; chown bj:bj "$res.tmp" 2>/dev/null + mv -f "$res.tmp" "$res" +} +shopt -s nullglob +while true; do + for req in "$REQDIR"/*.req; do process_one "$req"; done + sleep 1 +done diff --git a/deploy/linux/sbin/bj-quikexport-admin b/deploy/linux/sbin/bj-quikexport-admin new file mode 100755 index 0000000..7f82098 --- /dev/null +++ b/deploy/linux/sbin/bj-quikexport-admin @@ -0,0 +1,121 @@ +#!/bin/bash +# bj-quikexport-admin — привилегированные операции модуля «Экспорт биржевой +# информации» (ARQA quikexport). Вызывается root-воркером bj-keymedia-worker +# по запросам bj-server. Печатает JSON {"ok":true,...} или {"ok":false,"error":...}. +set -u +ACTION="${1:-}"; STAGING="${2:-}"; EXTRA="${BJ_EXTRA:-}" +Q=/opt/arqasrv/quik/utils/quikexport; SVC=quikexport; OWNER=arqasrv +json(){ python3 -c 'import json,sys;print(json.dumps(json.loads(sys.argv[1]),ensure_ascii=False))' "$1"; } +err(){ python3 -c 'import json,sys;print(json.dumps({"ok":False,"error":sys.argv[1]},ensure_ascii=False))' "$1"; exit 0; } +b64(){ base64 -w0 "$1" 2>/dev/null || true; } +case "$ACTION" in + dump) + [ -d "$Q" ] || err "каталог экспортёра $Q не найден" + python3 - "$Q" <<'PY' +import json,sys,base64,os +q=sys.argv[1]; out={"ok":True,"dir":q} +for n in ("export.ini","info.ini"): + p=os.path.join(q,n) + out[n]=base64.b64encode(open(p,'rb').read()).decode() if os.path.exists(p) else "" +out["keys"]=[n for n in ("pubring.txk","secring.txk") if os.path.exists(os.path.join(q,n))] +out["keyfiles"]={n:base64.b64encode(open(os.path.join(q,n),'rb').read()).decode() for n in out["keys"]} +print(json.dumps(out,ensure_ascii=False)) +PY + ;; + apply) + case "$STAGING" in /var/lib/bj/quik-export-apply/*) [ -d "$STAGING" ] || err "нет папки staging" ;; *) err "staging вне зоны" ;; esac + [ -d "$Q" ] || err "каталог экспортёра $Q не найден — сначала установите модуль" + TS=$(date +%Y%m%d-%H%M%S); BK="$Q/bj-backup-$TS"; mkdir -p "$BK" + for f in export.ini info.ini pubring.txk secring.txk; do [ -f "$Q/$f" ] && cp -p "$Q/$f" "$BK/"; done + applied=() + for f in export.ini info.ini pubring.txk secring.txk; do + if [ -f "$STAGING/$f" ]; then install -o $OWNER -g $OWNER -m 640 "$STAGING/$f" "$Q/$f" && applied+=("$f"); fi + done + if [ -f "$STAGING/odbc.ini" ]; then + python3 - "$STAGING/odbc.ini" <<'PY' +import sys,re +new=open(sys.argv[1]).read().strip()+"\n" +p="/etc/odbc.ini" +try: cur=open(p).read() +except FileNotFoundError: cur="" +# заменяем/добавляем секцию [quik_export] +parts=re.split(r'(?m)^(?=\[)',cur); keep=[s for s in parts if s.strip() and not s.startswith('[quik_export]')] +open(p,'w').write("".join(keep).rstrip()+("\n\n" if keep else "")+new) +PY + applied+=("odbc.ini") + fi + # ротация логов экспортёра (quikapi.log растёт ~1.8 ГБ/сутки) + cat > /etc/logrotate.d/quikexport </dev/null | tail -n +11 | xargs -r rm -rf + svc="none" + case "$EXTRA" in + restart) systemctl daemon-reload 2>/dev/null; systemctl restart $SVC >/dev/null 2>&1 && svc="restarted" || svc="restart-failed" ;; + stop) systemctl stop $SVC >/dev/null 2>&1 && svc="stopped" ;; + esac + python3 -c 'import json,sys;print(json.dumps({"ok":True,"applied":sys.argv[1].split(),"backup":sys.argv[2],"service":sys.argv[3]},ensure_ascii=False))' "${applied[*]:-}" "$BK" "$svc" + ;; + service) + case "$EXTRA" in + start|stop|restart) systemctl daemon-reload 2>/dev/null; out=$(systemctl "$EXTRA" $SVC 2>&1 | grep -v '^Warning:') ; systemctl is-active --quiet $SVC || [ "$EXTRA" = stop ] && json "{\"ok\":true,\"op\":\"$EXTRA\"}" || err "systemctl $EXTRA: $out" ;; + enable) systemctl enable $SVC >/dev/null 2>&1; json '{"ok":true,"op":"enable"}' ;; + *) err "неизвестная операция службы" ;; + esac + ;; + db-init) + case "$STAGING" in /var/lib/bj/quik-export-apply/*) [ -f "$STAGING/db.json" ] || err "нет db.json" ;; *) err "staging вне зоны" ;; esac + python3 - "$STAGING" <<'PY' +import json,sys,subprocess,os +st=sys.argv[1]; d=json.load(open(os.path.join(st,"db.json"))) +db,u,p,ru,rp=d["db"],d["user"],d["password"],d["ro_user"],d["ro_password"] +def psql(sql,dbname="postgres"): + r=subprocess.run(["sudo","-u","postgres","psql","-v","ON_ERROR_STOP=1","-Atq","-d",dbname,"-c",sql],capture_output=True,text=True) + if r.returncode: raise SystemExit(json.dumps({"ok":False,"error":r.stderr.strip()[-400:]},ensure_ascii=False)) + return r.stdout.strip() +q=lambda s: "'"+s.replace("'","''")+"'" +steps=[] +if psql(f"select 1 from pg_roles where rolname={q(u)}")!="1": psql(f"create role {u} login password {q(p)}"); steps.append("роль "+u) +else: psql(f"alter role {u} with password {q(p)}") +if psql(f"select 1 from pg_roles where rolname={q(ru)}")!="1": psql(f"create role {ru} login password {q(rp)}"); steps.append("роль "+ru) +else: psql(f"alter role {ru} with password {q(rp)}") +psql(f"alter role {u} set log_min_messages = fatal") +created=False +if psql(f"select 1 from pg_database where datname={q(db)}")!="1": + psql(f"create database {db} owner {u} encoding 'UTF8' template template0 lc_collate 'C.UTF-8' lc_ctype 'C.UTF-8'"); steps.append("база "+db); created=True +if created or psql("select count(*) from pg_tables where schemaname='public' and tablename='Portfolio'",db)!="1": + env=dict(os.environ,PGPASSWORD=p) + for f in ("exporter_pg.sql","npr_migration.sql"): + r=subprocess.run(["psql","-v","ON_ERROR_STOP=1","-q","-h","localhost","-U",u,"-d",db,"-f",os.path.join(st,f)],capture_output=True,text=True,env=env) + if r.returncode: raise SystemExit(json.dumps({"ok":False,"error":f+": "+r.stderr.strip()[-400:]},ensure_ascii=False)) + steps.append("схема "+f) +psql(f"grant connect on database {db} to {ru}",) +for s in ("public","npr"): + psql(f"grant usage on schema {s} to {ru}; grant select on all tables in schema {s} to {ru}; alter default privileges for role {u} in schema {s} grant select on tables to {ru}",db) +print(json.dumps({"ok":True,"steps":steps or ["база уже существует, права обновлены"]},ensure_ascii=False)) +PY + ;; + install) + case "$STAGING" in /var/lib/bj/quik-export-apply/*) : ;; *) err "staging вне зоны" ;; esac + [ -d "$Q" ] && err "экспортёр уже установлен в $Q" + RUN=$(ls "$STAGING"/install_quikexport-*.run 2>/dev/null | head -1); [ -n "$RUN" ] || err "в дистрибутиве нет install_quikexport-*.run" + chmod +x "$RUN"; out=$(cd "$STAGING" && "$RUN" -i 2>&1) || err "установщик: ${out: -300}" + apt-get install -y -q libjemalloc2 unixodbc odbc-postgresql libpq5 >/dev/null 2>&1 || true + mkdir -p /etc/systemd/system/$SVC.service.d + printf '[Unit]\nAfter=network-online.target postgresql.service\nWants=network-online.target\n[Service]\nRestart=on-failure\nRestartSec=15\nMemoryMax=3G\n[Install]\nWantedBy=multi-user.target\n' > /etc/systemd/system/$SVC.service.d/override.conf + systemctl daemon-reload; systemctl enable $SVC >/dev/null 2>&1 + json '{"ok":true,"installed":true}' + ;; + *) err "неизвестное действие $ACTION" ;; +esac diff --git a/deploy/linux/sbin/bj-validata-rebuild-gdbm b/deploy/linux/sbin/bj-validata-rebuild-gdbm new file mode 100755 index 0000000..603533b --- /dev/null +++ b/deploy/linux/sbin/bj-validata-rebuild-gdbm @@ -0,0 +1,102 @@ +#!/bin/bash +# bj-validata-rebuild-gdbm SPRDIR +# Пересобирает СОВМЕСТИМЫЙ справочник сертификатов (local.gdbm) из сертификатов, +# вшитых в архивные local.pse + local.gdbm НРД. Формат НРД-gdbm не читается +# gdbm-библиотекой («malformed»), но DER-сертификаты внутри — стандартный X.509. +# Порядок критичен: leaf-серты → CA → CRL (иначе E070003A «нет доверенного»). +# Справочник подписывается ключом с НОСИТЕЛЯ (нужен примонтированный ключевой +# носитель + vdmkdev). Сборка во временной папке — провал не портит оригинал. +set -uo pipefail +VDLIB=/opt/Validata/VDCSP/lib/amd64 +ZPKI=/opt/Validata/VDCSP/bin/amd64/zpki1utl +SPR="${1:?usage: bj-validata-rebuild-gdbm SPRDIR}" +PSE="$SPR/local.pse"; GDBM="$SPR/local.gdbm" +[ -f "$PSE" ] || { echo "{\"ok\":false,\"error\":\"нет $PSE\"}"; exit 1; } +WORK=$(mktemp -d); trap 'rm -rf "$WORK"' EXIT +cp "$PSE" "$WORK/local.pse" + +# 1) извлечь серты (leaf=subject!=issuer первыми, CA=self-signed потом) + CRL +EX=$(python3 - "$PSE" "$GDBM" "$WORK" <<'PY' +import sys,subprocess,hashlib +pse,gdbm,work=sys.argv[1],sys.argv[2],sys.argv[3] +leaves={}; cas={}; crls={} +for src in (pse,gdbm): + try: data=open(src,'rb').read() + except: continue + i=0 + while ibest[iss][0]: best[iss]=(t,h) +keep={h for _,h in best.values()} +for h in list(crls): + if h not in keep: print('dropped older CRL',file=sys.stderr); del crls[h] +n=0 +for h,b in crls.items(): open(f"{work}/3crl_{n}.der","wb").write(b); n+=1 +print(f"{len(leaves)} {len(cas)} {len(crls)}") +PY +) || { echo "{\"ok\":false,\"error\":\"извлечение сертов не удалось\"}"; exit 1; } + +CERTARGS=""; for c in "$WORK"/1leaf_*.der "$WORK"/2ca_*.der; do [ -f "$c" ] && CERTARGS="$CERTARGS -dercer $c"; done +CRLARGS=""; for c in "$WORK"/3crl_*.der; do [ -f "$c" ] && CRLARGS="$CRLARGS -dercrl $c"; done +[ -n "$CERTARGS" ] || { echo "{\"ok\":false,\"error\":\"сертификаты не извлечены ($EX)\"}"; exit 1; } + +# 2) пересобрать gdbm во временной папке (ключ подписи стора — с носителя) +OUT=$(LD_LIBRARY_PATH="$VDLIB" "$ZPKI" -minimal -tools -crestor \ + -uripse "pse://signed//$WORK/local.pse" -uriloc "file://$WORK/local.gdbm" \ + $CERTARGS $CRLARGS 2>&1) +if ! echo "$OUT" | grep -qi 'Результат: OK'; then + ERR=$(echo "$OUT" | grep -iE 'ошибк|Результат' | head -1 | tr -d '"') + echo "{\"ok\":false,\"error\":\"crestor: $ERR\"}"; exit 1 +fi +[ -f "$WORK/local.gdbm" ] || { echo "{\"ok\":false,\"error\":\"gdbm не создан\"}"; exit 1; } + +# 3) положить пересобранные на место (атомарно) +cp "$WORK/local.gdbm" "$GDBM.new" && mv "$GDBM.new" "$GDBM" +cp "$WORK/local.pse" "$PSE.new" && mv "$PSE.new" "$PSE" +echo "{\"ok\":true,\"leaf\":${EX% * *},\"crl\":${EX##* }}" diff --git a/deploy/systemd/bj-keymedia.service b/deploy/systemd/bj-keymedia.service new file mode 100644 index 0000000..9fe9755 --- /dev/null +++ b/deploy/systemd/bj-keymedia.service @@ -0,0 +1,9 @@ +[Unit] +Description=bj-server keymedia install worker (privileged daemon, host namespace) +[Service] +Type=simple +ExecStart=/usr/local/sbin/bj-keymedia-worker +Restart=always +RestartSec=2 +[Install] +WantedBy=multi-user.target