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
@@ -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##* }}"