chore(deploy): обновление ИШ v107, keymedia, экспортёр QUIK, fail2ban для bj
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
Executable
+181
@@ -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" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
# headless-запуск igate-svc v107: gnome-keyring + виртуальный дисплей.
|
||||
# Запускается из ExecStart через dbus-run-session (сессионная шина уже есть).
|
||||
export HOME=$HOME_DIR
|
||||
export XDG_DATA_HOME="\$HOME/.local/share"
|
||||
export XDG_RUNTIME_DIR="/run/user/\$(id -u)"
|
||||
export GIO_USE_VFS=local
|
||||
eval "\$(printf '%s\n' "\$(cat $KEYRING_PASS_FILE)" | gnome-keyring-daemon --replace --unlock --components=secrets,pkcs11 2>/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" <<EOF
|
||||
[Unit]
|
||||
StartLimitIntervalSec=300
|
||||
StartLimitBurst=8
|
||||
|
||||
[Service]
|
||||
User=$SVC_USER
|
||||
WorkingDirectory=$HOME_DIR
|
||||
# САМООЧИСТКА stale-состояния перед стартом — иначе рестарты флакуют
|
||||
# (осиротевшие CoreFxPipe-сокеты / login.keyring / X-локи).
|
||||
ExecStartPre=-/bin/bash -c 'rm -f /tmp/CoreFxPipe_NSD.IGate.* $HOME_DIR/.local/share/keyrings/login.keyring; find /tmp -maxdepth 1 -name ".X9*-lock" -delete 2>/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 <<MSG
|
||||
|
||||
⚠️ Web API 8090 не поднялся. Диагностика:
|
||||
• лог движка: /var/lib/$SVC_USER/.local/share/NSD Integration Gateway/logs-svc/log.txt
|
||||
(искать RefreshToken/Unable to start/fingerprint/HTTP)
|
||||
• если 'CA fingerprint mismatch' — сервер покажет actual-отпечаток, вписать его
|
||||
в wslTlsCACertFingerprint (не перепутать контур/крипту GOST↔RSA)
|
||||
• если RefreshToken пустой — проверить wslEndpoint (должен быть КОРОТКИМ базовым)
|
||||
Откат на предыдущую версию:
|
||||
systemctl stop igate && rm -rf $IGATE_DIR &&
|
||||
tar xzf $BK/opt-nsd-igate.tar.gz -C $(dirname "$IGATE_DIR") &&
|
||||
printf '[Service]\nWorkingDirectory=$HOME_DIR\n' > $DROPIN_DIR/override.conf &&
|
||||
systemctl daemon-reload && systemctl start igate
|
||||
MSG
|
||||
fi
|
||||
Executable
+108
@@ -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/<ver>/igate-<ver>-amd64.zip
|
||||
# (внутри igate_<ver>_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=<id устанавливающего>, WD=/home).
|
||||
mkdir -p "$DROPIN"
|
||||
if [ -d "$WORK/dropin-backup" ]; then
|
||||
cp -a "$WORK/dropin-backup/." "$DROPIN/"
|
||||
else
|
||||
cat > "$DROPIN/override.conf" <<EOF
|
||||
[Service]
|
||||
User=$SVC_USER
|
||||
WorkingDirectory=/var/lib/igate
|
||||
EOF
|
||||
fi
|
||||
# На случай, если postinst выставил числовой User= в основном юните — приведём к igate.
|
||||
sed -i "s/^User=.*/User=$SVC_USER/" /etc/systemd/system/igate.service 2>/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 <<EOF
|
||||
Дальнейшие шаги (TEST3, вручную либо через configure-ish.sql):
|
||||
• параметры канала WSL для TEST3 миграцией НЕ переносятся — применить наш
|
||||
configure-ish.sql (channels/parameters переживают обновление, но сверить);
|
||||
• при ошибке «Сертификат издателя не найден» — пересобрать профиль
|
||||
Справочника сертификатов (Валидата);
|
||||
• для M2M включить: флаг «M2M переводы», fetchInterval=00:00:10,
|
||||
время ожидания перед отправкой=00:00:01, папка ERRORS, автоуведомления
|
||||
о приёме транзита, ignorePackageDirectoryStructure=True;
|
||||
• config.xml businessLine=MOST — в bj: Настройка → НРД → «ИШ 107.0.0.904+».
|
||||
Откат: systemctl stop igate && rm -rf $IGATE_DIR &&
|
||||
tar xzf /var/backups/igate-$STAMP.tar.gz -C $(dirname "$IGATE_DIR") &&
|
||||
systemctl start igate
|
||||
EOF
|
||||
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
# fail2ban: перебор паролей входа в bj (строки auth.go: «auth: отказ входа "логин" с IP: причина»).
|
||||
# Дату в начале строки fail2ban вырезает сам; без якоря ^ — с ним строка не совпадает.
|
||||
[Definition]
|
||||
failregex = auth: отказ входа "[^"]*" с <HOST>:
|
||||
ignoreregex =
|
||||
@@ -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
|
||||
Executable
+208
@@ -0,0 +1,208 @@
|
||||
#!/bin/bash
|
||||
# bj-keymedia-install — установка ключа Валидаты на съёмный носитель.
|
||||
# Сохраняет структуру архива НРД (key.reg + spr<N>/ + vdkeys/) в профиль на
|
||||
# носителе, ПЕРЕСОБИРАЕТ совместимый справочник сертификатов (архивный gdbm НРД
|
||||
# несовместим — bj-validata-rebuild-gdbm чинит), и формирует профиль для ИШ.
|
||||
# inspect <staging>
|
||||
# install <staging> [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<L<20000 and len(b)==4+L:
|
||||
r=subprocess.run(['openssl','x509','-inform','DER','-noout','-subject','-issuer','-serial','-dates','-nameopt','utf8,sep_comma_plus'],input=b,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL)
|
||||
if r.returncode==0:
|
||||
o=r.stdout.decode('utf-8','replace'); g=lambda k:(re.search('^'+k+'=(.*)$',o,re.M) or [None,''])[1].strip()
|
||||
cn=lambda v:(re.search(r'CN=([^,]+)',v) or [None,''])[1].strip()
|
||||
sn=g('serial').upper()
|
||||
if sn in seen: i+=4; continue
|
||||
seen.add(sn)
|
||||
out.append({'kind':'x509','store':store,'subject':g('subject'),'subject_cn':cn(g('subject')),'issuer_cn':cn(g('issuer')),'serial':sn,'not_before':g('notBefore'),'not_after':g('notAfter'),'self_signed':g('subject')==g('issuer')})
|
||||
else:
|
||||
r=subprocess.run(['openssl','crl','-inform','DER','-noout','-issuer','-nextupdate','-nameopt','utf8,sep_comma_plus'],input=b,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL)
|
||||
if r.returncode==0:
|
||||
o=r.stdout.decode('utf-8','replace'); m=re.search(r'CN=([^,\n]+)',o); n=re.search(r'nextUpdate=(.*)',o)
|
||||
out.append({'kind':'crl','store':store,'issuer_cn':(m.group(1).strip() if m else ''),'not_after':(n.group(1).strip() if n else '')})
|
||||
i+=4
|
||||
print(json.dumps(out,ensure_ascii=False))
|
||||
PY2
|
||||
}
|
||||
if [ "$ACTION" = "certs-list" ]; then
|
||||
prof_dir
|
||||
echo "{\"ok\":true,\"profile\":\"$PROFILE_NAME\",\"dir\":\"$PROFDIR\",\"certs\":$(certs_list_json)}"; exit 0
|
||||
fi
|
||||
if [ "$ACTION" = "certs-apply" ]; then
|
||||
prof_dir
|
||||
case "$STAGING" in /var/lib/bj/nsd-certs-apply/*) [ -d "$STAGING" ] || err "нет папки с сертификатами" ;; *) err "staging вне зоны" ;; esac
|
||||
TS=$(date +%Y%m%d-%H%M%S); BK="$BACKUP_DIR/certs-$PROFILE_NAME-$TS"; mkdir -p "$BK"
|
||||
cp -a "$PROFDIR"/local.pse "$PROFDIR"/local.gdbm "$BK"/ || err "бэкап справочника не удался"
|
||||
restore(){ cp -a "$BK"/local.pse "$BK"/local.gdbm "$PROFDIR"/; chown igate:igate "$PROFDIR"/local.*; chmod 644 "$PROFDIR"/local.*; }
|
||||
OUT=$(EXTRA_DIR="$STAGING" EXCLUDE_SERIALS="${BJ_EXTRA:-}" "$REBUILD" "$PROFDIR" 2>"$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>
|
||||
[ -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<N>/ + 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<len(L):
|
||||
l=L[i]
|
||||
if l.strip().startswith("[Profile_") :
|
||||
# прочитать имя блока
|
||||
blk=[l]; j=i+1
|
||||
while j<len(L) and not L[j].strip().startswith("["):
|
||||
blk.append(L[j]); j+=1
|
||||
if any(b.strip()==f"Name = {name}" for b in blk):
|
||||
i=j; continue # выкинуть старый одноимённый
|
||||
out+=blk; i=j; continue
|
||||
out.append(l); i+=1
|
||||
L=out
|
||||
# найти max индекс Profile_N
|
||||
idx=-1
|
||||
for l in L:
|
||||
s=l.strip()
|
||||
if s.startswith("[Profile_") and s.endswith("]"):
|
||||
try: idx=max(idx,int(s[len("[Profile_"):-1]))
|
||||
except: pass
|
||||
n=idx+1
|
||||
# нарастить Count в секции верхнеуровневых профилей (последний 'Count = K' до первого Profile_)
|
||||
firstp=next((k for k,l in enumerate(L) if l.strip().startswith("[Profile_")), len(L))
|
||||
for k in range(firstp-1,-1,-1):
|
||||
if L[k].strip().startswith("Count = "):
|
||||
try: L[k]=f"Count = {int(L[k].split('=')[1])+1}"
|
||||
except: pass
|
||||
break
|
||||
L+=[f"[Profile_{n}]",f"Name = {name}","Count = 2",
|
||||
f"store_0 = pse://signed//{igp}/local.pse",
|
||||
f"store_1 = file://{igp}/local.gdbm"]
|
||||
open(ini,"w").write("\n".join(L))
|
||||
PY
|
||||
fi
|
||||
# подключить профиль к ИШ: cryptography.profile в БД igate (канал 33)
|
||||
if [ "$IGATE_RES" = "ok" ]; then
|
||||
if su -s /bin/sh postgres -c "psql -d igate -tAc \"update parameters set value='$PROFILE' where name='cryptography.profile'\"" >/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"
|
||||
Executable
+22
@@ -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
|
||||
Executable
+121
@@ -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 <<EOF
|
||||
$Q/logs/*.log $Q/logs/*.err {
|
||||
daily
|
||||
maxsize 200M
|
||||
rotate 5
|
||||
compress
|
||||
delaycompress
|
||||
missingok
|
||||
notifempty
|
||||
copytruncate
|
||||
su $OWNER $OWNER
|
||||
}
|
||||
EOF
|
||||
# ограничиваем старые бэкапы (оставляем 10)
|
||||
ls -1dt "$Q"/bj-backup-* 2>/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
|
||||
Executable
+102
@@ -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 i<len(data)-4:
|
||||
if data[i]==0x30 and data[i+1]==0x82:
|
||||
ln=(data[i+2]<<8)|data[i+3]; blob=data[i:i+4+ln]
|
||||
if len(blob)==4+ln:
|
||||
x=subprocess.run(['openssl','x509','-inform','DER','-noout','-subject','-issuer'],input=blob,capture_output=True)
|
||||
if x.returncode==0:
|
||||
o=x.stdout.decode()
|
||||
subj=[l for l in o.split(chr(10)) if l.startswith('subject')][0][7:]
|
||||
iss=[l for l in o.split(chr(10)) if l.startswith('issuer')][0][6:]
|
||||
(cas if subj==iss else leaves)[hashlib.md5(blob).hexdigest()]=blob
|
||||
elif subprocess.run(['openssl','crl','-inform','DER','-noout'],input=blob,capture_output=True).returncode==0:
|
||||
crls[hashlib.md5(blob).hexdigest()]=blob
|
||||
i+=1
|
||||
# внешние сертификаты НРД (напр. новый серт крипто-сервера) — берём только те, чей издатель уже есть среди CA хранилища
|
||||
import os,glob
|
||||
casubj=set()
|
||||
for b in cas.values():
|
||||
r=subprocess.run(['openssl','x509','-inform','DER','-noout','-subject'],input=b,capture_output=True); casubj.add(r.stdout.decode().strip()[8:])
|
||||
for f in sorted(glob.glob(os.environ.get('EXTRA_DIR','/var/lib/bj/.bj/nsd-certs')+'/*.cer')):
|
||||
b=open(f,'rb').read()
|
||||
if b[:2]!=b'\x30\x82':
|
||||
r=subprocess.run(['openssl','x509','-in',f,'-outform','DER'],capture_output=True); b=r.stdout
|
||||
x=subprocess.run(['openssl','x509','-inform','DER','-noout','-issuer'],input=b,capture_output=True)
|
||||
if x.returncode!=0: continue
|
||||
iss=x.stdout.decode().strip()[7:]
|
||||
if iss in casubj: leaves[hashlib.md5(b).hexdigest()]=b; print('extra:',os.path.basename(f),file=sys.stderr)
|
||||
else: print('extra skip (нет CA):',os.path.basename(f),file=sys.stderr)
|
||||
# исключить сертификаты по серийному номеру (напр. старый серт крипто-сервера НРД после смены ключа)
|
||||
excl={x.strip().upper().lstrip('0') for x in os.environ.get('EXCLUDE_SERIALS','').split(',') if x.strip()}
|
||||
if excl:
|
||||
for h in list(leaves):
|
||||
r=subprocess.run(['openssl','x509','-inform','DER','-noout','-serial'],input=leaves[h],capture_output=True)
|
||||
sn=r.stdout.decode().strip().split('=')[-1].upper().lstrip('0')
|
||||
if sn in excl: del leaves[h]; print('excluded serial:',sn,file=sys.stderr)
|
||||
n=0
|
||||
for h,b in leaves.items(): open(f"{work}/1leaf_{n}.der","wb").write(b); n+=1
|
||||
n=0
|
||||
for h,b in cas.items(): open(f"{work}/2ca_{n}.der","wb").write(b); n+=1
|
||||
# newest CRL: на каждого издателя оставить только самый свежий CRL — crestor падает
|
||||
# «имеется более новый САС» (E0700087), если после нового добавить старый
|
||||
import datetime
|
||||
best={}
|
||||
for h,b in crls.items():
|
||||
r=subprocess.run(['openssl','crl','-inform','DER','-noout','-issuer','-lastupdate'],input=b,capture_output=True)
|
||||
o=r.stdout.decode(errors='replace'); iss=[l for l in o.splitlines() if l.startswith('issuer')][0]
|
||||
lu=[l for l in o.splitlines() if l.startswith('lastUpdate')][0].split('=',1)[1].strip()
|
||||
try: t=datetime.datetime.strptime(lu,'%b %d %H:%M:%S %Y %Z')
|
||||
except Exception: t=datetime.datetime.min
|
||||
if iss not in best or t>best[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##* }}"
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user