chore(deploy): обновление ИШ v107, keymedia, экспортёр QUIK, fail2ban для bj

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zuevav
2026-09-15 19:41:11 +03:00
parent 86aa452ed6
commit 59fc774f08
11 changed files with 848 additions and 0 deletions
+69
View File
@@ -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;
+181
View File
@@ -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
+108
View File
@@ -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 =
+23
View File
@@ -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
+208
View File
@@ -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"
+22
View File
@@ -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
+121
View File
@@ -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
+102
View File
@@ -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##* }}"
+9
View File
@@ -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