#!/usr/bin/env bash # 3X-UI_KIT (форк hacker07) — локальные дополнения. # # Этот файл наш: автор его не знает и не правит, поэтому обновления автора его не ломают. # kit подключает его одной строкой из kit.sh (шов в tools/fork_apply.py) и передаёт сюда # аргументы командной строки. Всё, что мы дописываем к kit, живёт ЗДЕСЬ. # # Команды: kit local <команда> (справка: kit local help) # # Часть команд повторяет поведение проекта StressOzz/vps-setup (добавить порт SSH, отключить # ответ на ping, сменить пароль root). Там нет файла лицензии, поэтому код свой, а поведение # сделано безопаснее, чем в источнике: # * конфиг sshd проверяется (`sshd -t`) ДО перезапуска, новый порт сначала открывается в ufw; # * прежний порт продолжает работать, пока новый не проверен (`--close-old` — отдельный шаг); # * у каждой правки есть --dry-run и копия затронутого файла в /root/kit-local-backup-<дата>/; # * apt full-upgrade из источника НЕ выполняется (он тянет систему вперёд и может сломать # закреплённые версии панели и ядра) — вместо него автообновления безопасности. # # Правила работы с файлом: только функции, никакого кода на верхнем уровне (он выполняется при # каждом запуске kit), и никаких зависимостей от функций автора: наш вызов идёт в самом начале # kit.sh, когда авторские say/warn ещё не объявлены. KL_BACKUP_DIR="/root/kit-local-backup-$(date +%Y%m%d)" KL_SSHD_CONF="/etc/ssh/sshd_config" KL_SSHD_DROPIN="/etc/ssh/sshd_config.d/99-kit-local-port.conf" KL_SOCKET_DROPIN="/etc/systemd/system/ssh.socket.d/99-kit-local-port.conf" KL_UFW_RULES=/etc/ufw/before.rules KL_UFW6_RULES=/etc/ufw/before6.rules KL_SYSCTL_DROPIN=/etc/sysctl.d/99-kit-local-icmp.conf kl_say() { printf '\033[1m==>\033[0m %s\n' "$*"; } kl_ok() { printf ' ✓ %s\n' "$*"; } kl_warn() { printf ' ! %s\n' "$*" >&2; } kl_step() { printf '\n\033[1m%s\033[0m\n' "$*"; } kl_die() { printf '✗ %s\n' "$*" >&2; return 1; } kl_have() { command -v "$1" >/dev/null 2>&1; } kl_dry() { [[ ${KL_DRY:-no} == yes ]]; } kl_run() { if kl_dry; then printf ' (dry-run) %s\n' "$*"; return 0; fi; "$@"; } # Разбор аргументов: --dry-run поднимаем в KL_DRY, остальное складываем в KL_REST. kl_args() { KL_DRY=no; KL_REST=() local a for a in "$@"; do case $a in --dry-run) KL_DRY=yes ;; *) KL_REST+=("$a") ;; esac done } kl_backup() { # файл → копия рядом с релизом, не в /tmp (там её переживёт только до перезагрузки) [[ -e $1 ]] || return 0 local dst="$KL_BACKUP_DIR/$(printf '%s' "$1" | tr '/' '_')" if kl_dry; then printf ' (dry-run) сохранил бы %s → %s\n' "$1" "$dst"; return 0; fi install -d -m 700 "$KL_BACKUP_DIR" cp -p "$1" "$dst" && printf ' ✓ копия: %s\n' "$dst" return 0 } kl_need_root() { [[ ${EUID:-0} -eq 0 ]] || kl_die "нужен root"; } # ------------------------------------------------------------------ справка kit_local_help() { cat <<'EOF' 3X-UI_autoVPS — дополнения форка (hacker07): Состояние kit local эта справка kit local version версия kit, источник обновлений, дата сборки нашего файла kit local channel откуда берутся обновления и чем проверяется подпись kit local seams какие места форк правит в файлах автора kit local masking состояние маскировки (у REALITY должен быть свой домен) kit local hardening сводка по защите сервера и что ещё можно включить kit local links подписка, QR-код и ссылки по протоколам (как в меню kit) kit local ports открытые порты: что нужно, что лишнее и как закрыть kit local uninstall то же, что kit uninstall: снести всё, включая сам kit (SSH не трогает) Заглушка (сайт на 443 и ответ Hysteria2 — одна и та же страница) kit local site поставить случайный сайт из нашего набора (10 штук) kit local site --list список сайтов с номерами kit local site 7 поставить конкретный kit local site --show какой сайт стоит сейчас Защита сервера (поведение взято из StressOzz/vps-setup, сделано безопаснее) kit local ssh 2222 добавить порт SSH (прежние продолжают работать) kit local ssh --status какие порты SSH слушаются сейчас kit local ssh --close-old 2222 закрыть прежние порты после проверки нового kit local ping off|on отключить/вернуть ответ на ICMP echo (ping) через ufw kit local ping status состояние ответа на ping kit local passwd [юзер] сменить пароль (спросит дважды, в логи не попадает) kit local passwd --generate сгенерировать пароль и показать один раз kit local upgrades on|off|status автообновления безопасности У любой правки можно добавить --dry-run: покажет план и ничего не изменит. Копии изменённых файлов — в /root/kit-local-backup-<дата>/. Оговорки: * прежний порт SSH сам не закрывается: сначала проверьте новый из отдельного окна; * ping отключаем правилом ufw (видно в `ufw status`), а не общесистемным sysctl: отменяется одной командой и не глушит диагностику для внутренних интерфейсов; * apt full-upgrade из источника не делаем: вместо него — обновления безопасности. EOF } kit_local_version() { printf 'kit: %s (форк hacker07)\n' "${KIT_VERSION:-?}" printf 'источник: %s\n' "${KIT_RAW:-?}" local lib=${KIT_LOCAL_LIB:-/usr/local/lib/kit/kit-local.sh} printf 'наш файл: %s\n' "$lib" [[ -r $lib ]] && printf 'собран: %s\n' "$(date -r "$lib" '+%Y-%m-%d %H:%M' 2>/dev/null || echo '?')" local latest latest=$(cat /etc/kit/latest-version 2>/dev/null || true) [[ -n $latest ]] && printf 'последняя: %s\n' "$latest" return 0 } kit_local_channel() { printf 'файлы релиза: %s\n' "${KIT_RAW:-?}" printf 'подпись: id=%s namespace=%s\n' "${KIT_SIG_ID:-?}" "${KIT_SIG_NS:-?}" local n=0 k for k in "${KIT_SIGNERS[@]:-}"; do [[ -n $k ]] && n=$((n + 1)); done printf 'ключей в KIT_SIGNERS: %d (должен быть только ключ форка)\n' "$n" if [[ $n -eq 0 ]]; then printf 'ВНИМАНИЕ: список ключей пуст — обновление не пройдёт проверку подписи.\n' fi return 0 } kit_local_seams() { local bin=/usr/local/bin/kit [[ -r $bin ]] || { printf 'нет %s\n' "$bin"; return 1; } grep -n '3X-UI_KIT (форк hacker07)' "$bin" 2>/dev/null \ || printf 'метки форка в %s не найдены — сборка не наша?\n' "$bin" return 0 } kit_local_masking() { kl_have kit || { printf 'нет команды kit\n'; return 1; } kit net site 2>/dev/null || printf 'посмотреть: kit net site\n' return 0 } # ------------------------------------------------------------------ SSH: порты kl_ssh_ports() { { sshd -T 2>/dev/null | awk '$1 == "port" {print $2}' kl_have ss && ss -H -ltnp 2>/dev/null | awk '/sshd/ {n = split($4, a, ":"); print a[n]}' [[ -n ${SSH_CONNECTION:-} ]] && awk '{print $4}' <<<"$SSH_CONNECTION" [[ -n ${SSH_CLIENT:-} ]] && awk '{print $2}' <<<"$SSH_CLIENT" } 2>/dev/null | grep -E '^[0-9]+$' | sort -un | tr '\n' ' ' } kl_ssh_socket_active() { kl_have systemctl && systemctl is-active --quiet ssh.socket 2>/dev/null; } kl_ufw_active() { kl_have ufw && ufw status 2>/dev/null | grep -q '^Status: active'; } kl_ssh_status() { local ports; ports=$(kl_ssh_ports) printf 'порты SSH сейчас: %s\n' "${ports:-не удалось определить}" if kl_ssh_socket_active; then printf 'слушает: systemd (ssh.socket — так в Ubuntu 24.04)\n' [[ -f $KL_SOCKET_DROPIN ]] && grep -H 'ListenStream' "$KL_SOCKET_DROPIN" else printf 'слушает: sshd по %s\n' "$KL_SSHD_CONF" [[ -f $KL_SSHD_DROPIN ]] && grep -H '^Port' "$KL_SSHD_DROPIN" fi kl_ufw_active && printf 'ufw: %s\n' "$(ufw status 2>/dev/null | awk '$2 == "ALLOW" && $1 ~ /\/tcp$/ {printf "%s ", $1}')" return 0 } kit_local_ssh() { kl_args "$@" local arg=${KL_REST[0]:-} second=${KL_REST[1]:-} close_old=no new [[ $arg == --close-old ]] && { close_old=yes; new=$second; } [[ $second == --close-old ]] && close_old=yes if [[ -z $arg || $arg == --status ]]; then kl_ssh_status; return 0; fi kl_need_root || return 1 [[ $close_old == no ]] && new=$arg [[ $new =~ ^[0-9]+$ ]] && ((new >= 1 && new <= 65535)) \ || { kl_die "порт — число 1..65535, например: kit local ssh 2222"; return 1; } local cur; cur=$(kl_ssh_ports) if [[ $close_old == yes ]]; then kl_say "закрываю прежние порты SSH, оставляю только $new" printf ' было: %s\n' "${cur:-?}" if [[ -z $cur ]]; then kl_warn "не удалось определить текущие порты SSH — закройте прежний порт вручную (файл sshd или ssh.socket)" return 1 fi if kl_ssh_socket_active; then cat </dev/null 2>&1 kl_ok "порт $p закрыт" done kl_run sshd -t || { kl_warn "sshd -t не принял конфиг — верните файл из $KL_BACKUP_DIR"; return 1; } kl_run systemctl restart ssh kl_say "остался только порт $new. Проверьте вход в НОВОМ окне, это не закрывая." return 0 fi kl_say "добавляю порт SSH $new (прежние: ${cur:-?})" if [[ " $cur " == *" $new "* ]]; then kl_warn "порт $new уже слушается — ничего не меняю"; return 0; fi if kl_have ss && ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -qE "[:.]$new\$"; then kl_die "порт $new занят другой службой — выберите другой"; return 1 fi if kl_ufw_active; then kl_run ufw allow "$new/tcp" && kl_ok "ufw: открыт $new/tcp — сделано до перезапуска, чтобы не запереть себя" else kl_warn "ufw не активен: проверьте, что порт $new открыт у хостера" fi if kl_ssh_socket_active; then kl_backup "$KL_SOCKET_DROPIN" if kl_dry; then printf ' (dry-run) записал бы в %s: [Socket] + ListenStream=%s (старый порт остаётся)\n' "$KL_SOCKET_DROPIN" "$new" else install -d -m 755 "$(dirname "$KL_SOCKET_DROPIN")" printf '# 3X-UI_autoVPS (форк): дополнительный порт SSH, добавлен %s\n[Socket]\nListenStream=%s\n' "$(date +%F)" "$new" >"$KL_SOCKET_DROPIN" kl_ok "добавлен $KL_SOCKET_DROPIN" fi kl_run systemctl daemon-reload kl_run systemctl restart ssh.socket else kl_backup "$KL_SSHD_DROPIN" if kl_dry; then printf ' (dry-run) записал бы в %s: Port %s (старый порт остаётся)\n' "$KL_SSHD_DROPIN" "$new" else install -d -m 755 /etc/ssh/sshd_config.d printf '# 3X-UI_autoVPS (форк): дополнительный порт SSH, добавлен %s\nPort %s\n' "$(date +%F)" "$new" >"$KL_SSHD_DROPIN" kl_ok "добавлен $KL_SSHD_DROPIN" fi kl_run sshd -t || { kl_warn "sshd -t не принял конфиг — убираю наш файл, ничего не перезапускаю"; kl_dry || rm -f "$KL_SSHD_DROPIN"; return 1; } kl_run systemctl restart ssh || kl_run systemctl restart sshd fi if ! kl_dry; then sleep 2 if ss -H -ltn 2>/dev/null | awk '{print $4}' | grep -qE "[:.]$new\$"; then kl_ok "sshd слушает $new" else kl_warn "порт $new пока не слушается — смотрите: journalctl -u ssh -n 30"; fi fi cat < 2) заработало — закройте прежние порты: kit local ssh --close-old $new 3) поменяйте порт у себя в ~/.ssh/config и в мониторинге, если он там есть EOF return 0 } # ------------------------------------------------------------------ ping (ICMP echo) kl_ping_state() { if kl_have ufw && [[ -f $KL_UFW_RULES ]]; then grep -qE '^-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT' "$KL_UFW_RULES" && echo allow || echo deny elif [[ -f $KL_SYSCTL_DROPIN ]]; then grep -q 'icmp_echo_ignore_all = 1' "$KL_SYSCTL_DROPIN" && echo deny || echo allow else echo "?" fi } kit_local_ping() { kl_args "$@" local want=${KL_REST[0]:-status} case $want in status | "") printf 'ответ на ping: %s\n' "$(kl_ping_state)" if kl_have ufw && [[ -f $KL_UFW_RULES ]]; then printf ' ufw before.rules: echo-request %s\n' "$(grep -qE '^# -A ufw-before-input -p icmp --icmp-type echo-request' "$KL_UFW_RULES" && echo 'закрыт (строка закомментирована)' || echo 'открыт')" [[ -f $KL_UFW6_RULES ]] && printf ' ufw before6.rules: echo-request %s\n' "$(grep -qE '^# -A ufw6-before-input' "$KL_UFW6_RULES" && echo 'закрыт' || echo 'открыт')" fi printf ' sysctl icmp_echo_ignore_all=%s\n' "$(sysctl -n net.ipv4.icmp_echo_ignore_all 2>/dev/null || echo '?')" return 0 ;; off | on) ;; *) kl_die "kit local ping off|on|status"; return 1 ;; esac kl_need_root || return 1 if kl_ufw_active; then kl_backup "$KL_UFW_RULES"; kl_backup "$KL_UFW6_RULES" if kl_dry; then printf ' (dry-run) %s в %s и before6.rules правило «icmp echo-request», затем ufw reload\n' \ "$([[ $want == off ]] && echo 'закомментировал бы' || echo 'вернул бы')" "$KL_UFW_RULES" return 0 fi local f for f in "$KL_UFW_RULES" "$KL_UFW6_RULES"; do [[ -f $f ]] || continue if [[ $want == off ]]; then sed -i -E 's|^(-A ufw6?-before-input -p icmp6? --icmp6?-type echo-request -j ACCEPT)|# \1 # kit local: закрыт|' "$f" else sed -i -E 's|^# (-A ufw6?-before-input -p icmp6? --icmp6?-type echo-request -j ACCEPT).*$|\1|' "$f" fi done ufw reload >/dev/null 2>&1 || kl_warn "ufw reload не прошёл — проверьте: ufw status" rm -f "$KL_SYSCTL_DROPIN" if [[ $want == off ]]; then kl_ok "сервер больше не отвечает на ping" kl_warn "ICMP-мониторинг этого сервера из дома работать перестанет (проба «ping» даст сбой)" kl_warn "вернуть: kit local ping on" else kl_ok "сервер снова отвечает на ping" fi return 0 fi kl_warn "ufw не активен — использую общесистемный запрет ICMP echo (это шире: глушит ping и для внутренних интерфейсов)" if kl_dry; then printf ' (dry-run) записал бы %s с icmp_echo_ignore_all=%s\n' "$KL_SYSCTL_DROPIN" "$([[ $want == off ]] && echo 1 || echo 0)" return 0 fi printf 'net.ipv4.icmp_echo_ignore_all = %s\n' "$([[ $want == off ]] && echo 1 || echo 0)" >"$KL_SYSCTL_DROPIN" sysctl -p "$KL_SYSCTL_DROPIN" >/dev/null 2>&1 kl_ok "готово (файл $KL_SYSCTL_DROPIN); вернуть: kit local ping on" return 0 } # ------------------------------------------------------------------ пароль kit_local_passwd() { kl_args "$@" local user=${KL_REST[0]:-root} gen=no [[ $user == --generate ]] && { user=root; gen=yes; } [[ ${KL_REST[1]:-} == --generate ]] && gen=yes kl_need_root || return 1 id "$user" >/dev/null 2>&1 || { kl_die "нет пользователя $user"; return 1; } if [[ $gen == yes ]]; then if kl_dry; then printf ' (dry-run) сгенерировал бы пароль пользователю %s и показал бы один раз\n' "$user"; return 0; fi local pw; pw=$(openssl rand -base64 24 | tr -d '/+=' | cut -c1-20) printf '%s:%s\n' "$user" "$pw" | chpasswd || { kl_die "chpasswd не сработал"; return 1; } cat </dev/null sshd -T 2>/dev/null | grep -qi '^passwordauthentication no' \ && kl_warn "в sshd вход по паролю выключен — пароль пригодится только для консоли хостера" return 0 } # ------------------------------------------------------------------ автообновления безопасности kit_local_upgrades_status() { printf 'автообновления безопасности: %s\n' "$([[ -f /etc/apt/apt.conf.d/20auto-upgrades ]] && echo включены || echo выключены)" kl_have systemctl && printf ' служба unattended-upgrades: %s\n' "$(systemctl is-active unattended-upgrades 2>/dev/null || echo 'нет')" [[ -f /etc/apt/apt.conf.d/20auto-upgrades ]] && sed -n '1,4p' /etc/apt/apt.conf.d/20auto-upgrades | sed 's/^/ /' if [[ -f /var/log/unattended-upgrades/unattended-upgrades.log ]]; then printf ' последние запуски:\n' tail -n 3 /var/log/unattended-upgrades/unattended-upgrades.log | cut -c1-110 | sed 's/^/ /' fi return 0 } kit_local_upgrades() { kl_args "$@" local want=${KL_REST[0]:-status} case $want in status | "") kit_local_upgrades_status; return 0 ;; on | off) ;; *) kl_die "kit local upgrades on|off|status"; return 1 ;; esac kl_need_root || return 1 if [[ $want == off ]]; then kl_say "выключаю автообновления безопасности" kl_run rm -f /etc/apt/apt.conf.d/20auto-upgrades /etc/apt/apt.conf.d/52-kit-local-upgrades kl_run systemctl disable --now unattended-upgrades kl_ok "готово (пакет оставлен)" return 0 fi kl_say "включаю автообновления безопасности (только security, без перезагрузки)" if ! kl_dry && ! dpkg -s unattended-upgrades >/dev/null 2>&1; then DEBIAN_FRONTEND=noninteractive apt-get install -y -qq unattended-upgrades \ || { kl_die "пакет не поставился"; return 1; } fi if kl_dry; then printf ' (dry-run) записал бы /etc/apt/apt.conf.d/20auto-upgrades и 52-kit-local-upgrades, включил бы службу\n' return 0 fi cat >/etc/apt/apt.conf.d/20auto-upgrades <<'EOF' APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Unattended-Upgrade "1"; APT::Periodic::Download-Upgradeable-Packages "1"; APT::Periodic::AutocleanInterval "7"; EOF cat >/etc/apt/apt.conf.d/52-kit-local-upgrades <<'EOF' // 3X-UI_autoVPS (форк): только обновления безопасности, никакой самовольной перезагрузки. Unattended-Upgrade::Automatic-Reboot "false"; Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; EOF systemctl enable --now unattended-upgrades >/dev/null 2>&1 || kl_warn "служба не включилась — проверьте: systemctl status unattended-upgrades" kl_ok "включено: обновления безопасности ставятся сами" kl_ok "панель и ядро это не затрагивает — они ставятся не из apt, а из своих релизов" kl_warn "обновление ядра применится только после перезагрузки, сам сервер не перезагружается" return 0 } # ------------------------------------------------------------------ сводка kit_local_hardening() { kl_args "$@" local ports; ports=$(kl_ssh_ports) kl_step "Защита сервера — состояние" printf 'порты SSH: %s\n' "${ports:-?}$(kl_ssh_socket_active && echo ' — через systemd ssh.socket')" printf 'ответ на ping: %s\n' "$(kl_ping_state)" printf 'ufw: %s\n' "$(kl_ufw_active && echo 'активен' || echo 'не активен')" kl_ufw_active && printf 'открытые порты: %s\n' "$(ufw status 2>/dev/null | awk '$2 == "ALLOW" {printf "%s ", $1}')" printf 'автообновления: %s\n' "$([[ -f /etc/apt/apt.conf.d/20auto-upgrades ]] && echo включены || echo выключены)" printf 'вход по паролю в sshd: %s\n' "$(sshd -T 2>/dev/null | awk '/^passwordauthentication/ {print $2}' || echo '?')" printf 'смена пароля root: %s\n' "$(chage -l root 2>/dev/null | awk -F': ' '/Last password change/ {print $2}')" printf 'продление сертификата: %s\n' "$(crontab -l 2>/dev/null | grep -c acme) задач acme в cron" cat <<'EOF' Что сделано намеренно МЯГЧЕ, чем в популярных скриптах первичной настройки: * apt full-upgrade — тянет систему вперёд и может сломать закреплённые панель и ядро; вместо него включены обновления безопасности; * автоматическая перезагрузка после правок — сервер не должен вставать без вашего решения; * общесистемный запрет ICMP для всех интерфейсов — здесь он выключен на уровне ufw: правило видно в `ufw status` и отменяется одной командой, а внутренняя диагностика не глушится. Причина одна: перечисленное может увести систему вперёд или выключить сервер, а панель и ядро у нас закреплённых версий. Что включить при желании: kit local ssh 2222 добавить порт SSH kit local ping off не отвечать на ping kit local upgrades on обновления безопасности kit local passwd --generate сменить пароль root EOF return 0 } # ------------------------------------------------------------------ заглушка: ИТ-сайты (наши) # Десять правдоподобных ИТ-сайтов: хостинг, DevOps, мониторинг, разработка, безопасность, # поддержка, сети и колокейшн, заметки про Linux, бэкапы, документация проекта. # Ставится случайный (kit local site), конкретный — по номеру (kit local site 7). # Страница отдаётся nginx на 443 при неизвестном SNI и синхронизируется в ответ Hysteria2 # (kit net masq on) — поэтому менять её нужно этой командой, иначе 443/tcp и UDP-порт начнут # отвечать разными страницами (это заметно снаружи). KL_SITE_FILE=/var/www/kit/index.html KL_SITE_TITLES=( "Radian Analytics — data platforms and BI" "Circuit Nine — DevOps consulting" "Cortex Monitor — server monitoring" "Bluefin Software — custom development" "Vantage Security — audits and penetration tests" "Keystone IT — managed systems support" "Meridian Networks — colocation and transit" "Lumen Notes — Linux and networking" "Evercore Backup — backup and recovery" "Halyard Docs — project documentation" ) KL_SITE_TOPICS=( "дата-аналитика: хранилище, отчёты, дашборды" "DevOps-консалтинг: CI/CD, Kubernetes, наблюдаемость" "мониторинг серверов: метрики, алерты, статус-страница" "разработка ПО на заказ: портфолио, стек, процесс" "аудит и пентест: этапы, отчёт, повторная проверка" "ИТ-поддержка: серверы, сети, рабочие места" "колокейшн и транзит: стойки, каналы, BGP" "заметки про Linux, nginx и сети" "бэкапы и восстановление: политика, проверки, тесты" "документация проекта: установка, конфигурация, справка" ) KL_SITE_CSS=':root{--fg:#1b1f24;--mu:#5b6670;--bd:#e3e6ea;--ac:#1f6feb;--bg:#fff} *{box-sizing:border-box} body{margin:0;font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;color:var(--fg);background:var(--bg)} .wrap{max-width:980px;margin:0 auto;padding:0 20px} header{border-bottom:1px solid var(--bd)} header .wrap{display:flex;align-items:center;justify-content:space-between;height:64px} .logo{font-weight:700;text-decoration:none;color:var(--fg)} nav a{color:var(--mu);text-decoration:none;margin-left:18px;font-size:15px} .hero{padding:72px 0 40px} .hero h1{font-size:40px;line-height:1.15;margin:0 0 16px;letter-spacing:-.02em} .hero p{font-size:18px;color:var(--mu);max-width:640px;margin:0 0 28px} .btn{display:inline-block;background:var(--ac);color:#fff;text-decoration:none;padding:12px 22px;border-radius:6px;font-weight:600} .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:20px;padding:8px 0 44px} .card{border:1px solid var(--bd);border-radius:8px;padding:22px} .card h3{margin:0 0 8px;font-size:18px} .card p{margin:0;color:var(--mu)} .prose{padding:0 0 56px;max-width:780px} .prose h2{font-size:22px;margin:34px 0 10px} .prose p,.prose li{color:var(--mu)} code{background:#f4f6f8;padding:2px 5px;border-radius:4px;font-size:14px} footer{border-top:1px solid var(--bd);padding:26px 0;color:var(--mu);font-size:14px} table{border-collapse:collapse;width:100%} th,td{border-bottom:1px solid var(--bd);text-align:left;padding:10px 8px;font-size:15px} ul{padding-left:20px} .modal{position:fixed;inset:0;display:none;align-items:center;justify-content:center;background:rgba(11,13,17,.55);padding:20px;z-index:50} .modal.on{display:flex} .modal .box{background:#fff;border-radius:10px;max-width:430px;padding:26px 26px 20px;box-shadow:0 18px 50px rgba(0,0,0,.28)} .modal h4{margin:0 0 8px;font-size:19px} .modal p{margin:0 0 18px;color:var(--mu)} .modal button{background:var(--ac);color:#fff;border:0;border-radius:6px;padding:10px 18px;font:inherit;font-weight:600;cursor:pointer} @media(max-width:600px){.hero{padding:44px 0 28px}.hero h1{font-size:29px}}' kl_site_open() { # заголовок описание бренд «навигация как HTML» cat < $1
HTML } kl_site_close() { # бренд cat <

© $(date +%Y) $1 · Imprint · Privacy · Status

HTML } kl_site_html() { # номер (с нуля) → страница на stdout case $1 in 0) # дата-аналитика kl_site_open "Radian Analytics — data platforms and BI" "We collect data from accounting, warehouses and websites into one place and build the few reports the business actually reads." "Radian Analytics" 'ServicesApproachContact' cat <<'HTML'

Reports that arrive before the meeting

We pull data out of your accounting system, warehouse and website, load it into one warehouse, and build the handful of dashboards people actually open.

What we build

Data pipelines

Scheduled loads from databases, APIs and spreadsheets, with checks that catch a broken source early.

Warehouse

PostgreSQL or ClickHouse, modelled so that a new question does not need a new table every time.

Dashboards

The five numbers that matter on one page, with the detail one click behind each of them.

How it usually goes

Two weeks to collect the sources and agree on definitions — most arguments turn out to be about definitions, not about software. Then the pipeline runs nightly and the first reports go out a week later.

One warning

A dashboard nobody opens is expensive decoration. One report that somebody uses beats ten that nobody does.

HTML kl_site_close "Nordhost" ;; 1) # DevOps-консалтинг kl_site_open "Circuit Nine — DevOps consulting" "We help small teams ship safely: CI/CD, Kubernetes, infrastructure as code and observability." "Circuit Nine" 'WorkApproachAbout' cat <<'HTML'

Ship on Fridays without holding your breath

We set up pipelines, infrastructure and monitoring so that releasing is a boring, repeatable event — and so that on-call does not ruin anyone’s week.

What we do

CI/CD

Build pipelines, artefact registries, staging environments and one-command rollbacks.

Infrastructure

Terraform and Ansible for servers and cloud, with reviews and tests in the pipeline.

Observability

Metrics, logs and traces wired together, with alerts that people actually trust.

How we work

A two-week discovery, then small changes with a review after each one. Everything lands in your repository and your accounts — no private tooling, no lock-in.

WeekWhat happens
1–2Audit of the current setup, a written report with priorities
3–6Pipelines and infrastructure, pairing with your engineers
7–8Monitoring, runbooks and a handover session
HTML kl_site_close "Circuit Nine" ;; 2) # мониторинг kl_site_open "Cortex Monitor — server monitoring" "Uptime, resource and certificate monitoring with alerting to Telegram, e-mail and webhooks." "Cortex Monitor" 'ChecksAlertsPricing' cat <<'HTML'

Know about the outage before your users do

HTTP, TCP, ICMP and TLS checks from six locations, host metrics through a lightweight agent, and alerts that reach you in seconds.

See checks

Availability

HTTP and TCP checks with keyword matching, redirects and status codes.

Resources

CPU, memory, disk and network of every host on one page.

Certificates

Expiry warnings 30, 14 and 3 days ahead, with a list of what needs renewing.

Alerting

Alerts are grouped so that a single broken router does not produce forty messages. Quiet hours, maintenance windows and escalation to a second contact are configurable per check.

Maybe you do not need us yet

If you run one server, an uptime check and a cron job with certificate renewal will do. Come back when there are more of them than you can remember.

HTML kl_site_close "Cortex Monitor" ;; 3) # разработка ПО kl_site_open "Bluefin Software — custom development" "A small team building web services, integrations and internal tools for companies that grew out of spreadsheets." "Bluefin Software" 'ServicesStackContact' cat <<'HTML'

Software for the part of the business nobody sells

Integrations, back-office tools and web services. We take a process that lives in three spreadsheets and one person’s head, and give it a proper home.

Services

Web services

APIs and admin panels in Python or Go, deployed to your infrastructure.

Integrations

Accounting, warehouses, CRM and everything that has an API or a database.

Rescue work

Taking over an unfinished project: audit, plan, tidy up, finish.

Stack

Python (Django, FastAPI), PostgreSQL, Redis, Docker, and whatever the customer already runs. We keep deployments simple enough that a new colleague can understand them in a day.

Team

Four engineers and a part-time designer. We take on three projects at a time so that nobody is queued behind somebody else’s deadline.

HTML kl_site_close "Bluefin Software" ;; 4) # безопасность kl_site_open "Vantage Security — audits and penetration tests" "External and internal penetration tests, configuration reviews and help with fixing what we find." "Vantage Security" 'ServicesProcessContact' cat <<'HTML'

Find the problems before somebody else does

Penetration tests and configuration reviews for web services, networks and cloud accounts — with a report your engineers can act on and a retest when the fixes are in.

Services

External test

Everything reachable from the internet: services, TLS, DNS, mail.

Internal test

What an attacker does after getting a foothold in an office or a VPN.

Review

Settings, backups, access rights and monitoring — usually the cheapest findings.

Process

Scope, rules of engagement and a test window are agreed in writing first. We work against a staging copy where possible, and we never touch production data beyond what the scope allows.

Report

Findings are ranked by what they let an attacker do, with reproduction steps and concrete fixes. A retest of the fixed issues is included in the price.

HTML kl_site_close "Vantage Security" ;; 5) # ИТ-поддержка kl_site_open "Keystone IT — managed systems support" "We look after servers, networks, backups and workstations for offices of ten to two hundred people." "Keystone IT" 'ServicesSupportContact' cat <<'HTML'

An IT department, without hiring one

Monitoring, updates, backups and a real person on the phone. Contracts start at a fixed monthly fee, and you can leave with thirty days’ notice.

What is included

Servers

Updates, monitoring, disk space and certificate renewal handled for you.

Backups

Nightly copies, offsite storage and a restore test twice a year.

Workplaces

Accounts, laptops, printers and the printer driver that never works.

Support

Weekdays from 8 to 19, with an on-call line for outages. Response time for a full outage is one hour, for ordinary requests one working day.

We keep a written inventory of your systems, so that a new engineer — ours or yours — can take over without a two-week interview with the person who built it.

HTML kl_site_close "Keystone IT" ;; 6) # сети и колокейшн kl_site_open "Meridian Networks — colocation and transit" "Rack space, cross-connects and IP transit in two carrier-neutral facilities." "Meridian Networks" 'ColocationTransitContact' cat <<'HTML'

Racks, cross-connects and transit

Quarter, half and full racks in Frankfurt and Amsterdam, with remote hands, redundant power and a choice of transit providers.

Colocation

Quarter rack

10 U, 2 kW, 1 Gbps port and one IPv4 prefix.

Half rack

22 U, 4 kW and a redundant 10 Gbps pair.

Private cage

From eight racks, with your own access control and monitoring.

Transit and peering

We announce your prefix over two upstreams and the local internet exchange. BGP communities for traffic engineering, RPKI filtering and a looking glass are available to customers.

PortCommitted rate
1 Gbpsfrom 50 Mbps
10 Gbpsfrom 1 Gbps
2×10 Gbpsfrom 2 Gbps, redundant
HTML kl_site_close "Meridian Networks" ;; 7) # блог kl_site_open "Lumen Notes — Linux and networking" "Short notes about servers, nginx, storage and networks — written while working, mostly for myself." "Lumen Notes" 'PostsAbout' cat <<'HTML'

Notes from a server room

Things I worked out once and do not want to work out again: nginx, systemd, ZFS, routing and the odd shell trick.

Read the posts

Replacing a certificate without dropping connections

Reload, not restart. systemctl reload nginx keeps the listeners open and swaps the configuration, so long downloads and websockets survive. A restart does not.

Finding what filled the disk

du -xh --max-depth=1 / | sort -h first, then follow the biggest branch. Journals under /var/log/journal are a common answer; journalctl --vacuum-size=200M trims them.

Checking that a port is really listening

ss -ltnp shows TCP listeners and the process behind each one. If something answers on 443 but is not in the list, look for a proxy in front of it.

Backup is not a copy

A copy on the same disk protects against nothing but a fat finger. The test that matters is a restore, done on purpose, on a machine that is not the original.

HTML kl_site_close "Lumen Notes" ;; 8) # бэкапы kl_site_open "Evercore Backup — backup and recovery" "Offsite backups with a documented restore procedure and a restore test every quarter." "Evercore Backup" 'PolicyRestoreContact' cat <<'HTML'

A backup you have never restored is a rumour

We copy your servers and databases to a separate location, keep versions, and prove once a quarter that a restore actually works.

How it works

Nightly copies

Files and databases, encrypted before they leave the server.

Versions

Hourly for a day, daily for a month, monthly for a year.

Restore test

A quarterly restore into a clean machine, with a short report.

Recovery

A lost file takes minutes: we restore the version you pick. A lost server takes an hour or two: a prepared image plus your latest copy, booted and checked before it goes back into service.

Encryption keys are kept by you as well as by us, so that neither side alone can read the copies.

HTML kl_site_close "Evercore Backup" ;; 9) # документация проекта kl_site_open "Halyard Docs — project documentation" "Installation, configuration and reference for a self-hosted service: everything in one place." "Halyard Docs" 'InstallConfigReference' cat <<'HTML'

Documentation

Installation, configuration and the reference pages. Written for the current release; older versions keep their own section.

Start here

Installation

Download the archive, unpack it into /opt/halyard and run the setup script. The script asks three questions and writes a configuration file next to the binary.

Configuration

Settings live in /etc/halyard/config.toml. Every option has a default, so a minimal file is often enough. Changes are applied with halyard reload — no restart, no dropped connections.

Reference

CommandWhat it does
halyard statusShows the running version and the state of each component
halyard checkValidates the configuration and the listening ports
halyard logsFollows the log with readable timestamps

Questions and patches are welcome through the issue tracker; the address is on the contact page.

HTML kl_site_close "Halyard Docs" ;; *) kl_die "нет такого сайта: $1" ;; esac } kl_site_current() { # какой из наших сайтов стоит сейчас (пусто — не наш) local i t [[ -s $KL_SITE_FILE ]] || return 0 for i in "${!KL_SITE_TITLES[@]}"; do t=${KL_SITE_TITLES[i]} if grep -qF "$t" "$KL_SITE_FILE" 2>/dev/null; then printf '%s' "$((i + 1)). $t" return 0 fi done return 0 } kit_local_site() { kl_args "$@" local arg=${KL_REST[0]:-} n cur case $arg in --list | list) kl_say "сайты-заглушки в наборе (ставится случайный)" local i for i in "${!KL_SITE_TITLES[@]}"; do printf ' %2d. %-22s %s\n' "$((i + 1))" "${KL_SITE_TITLES[i]%% — *}" "${KL_SITE_TOPICS[i]}" done printf ' %s\n' "поставить конкретный: kit local site 7" return 0 ;; --show | show | status) cur=$(kl_site_current) if [[ -n $cur ]]; then kl_ok "сейчас стоит наш сайт: $cur" elif [[ -s $KL_SITE_FILE ]]; then kl_ok "сейчас стоит страница автора (случайная из его наборов)" else kl_warn "файла $KL_SITE_FILE нет"; fi return 0 ;; "" | random | --random) n=$((RANDOM % ${#KL_SITE_TITLES[@]})) ;; *) [[ $arg =~ ^[0-9]{1,2}$ ]] && ((10#$arg >= 1 && 10#$arg <= ${#KL_SITE_TITLES[@]})) \ || { kl_die "kit local site [номер 1..${#KL_SITE_TITLES[@]}|--list|--show]"; return 1; } n=$((10#$arg - 1)) ;; esac if kl_dry; then kl_say "заглушка: ${KL_SITE_TITLES[n]} (${KL_SITE_TOPICS[n]})" printf ' (dry-run) записал бы %s и синхронизировал бы ответ Hysteria2 (kit net masq on)\n' "$KL_SITE_FILE" return 0 fi kl_need_root || return 1 kl_say "заглушка: ${KL_SITE_TITLES[n]} (${KL_SITE_TOPICS[n]})" install -d -m 755 /var/www/kit [[ -f $KL_SITE_FILE ]] && kl_backup "$KL_SITE_FILE" if ! kl_site_html "$n" >"$KL_SITE_FILE.new"; then rm -f "$KL_SITE_FILE.new"; kl_die "не удалось собрать страницу"; return 1 fi chmod 644 "$KL_SITE_FILE.new" && mv "$KL_SITE_FILE.new" "$KL_SITE_FILE" \ || { rm -f "$KL_SITE_FILE.new"; kl_die "не удалось заменить $KL_SITE_FILE"; return 1; } kl_ok "страница обновлена: $KL_SITE_FILE ($(wc -c <"$KL_SITE_FILE") байт)" # Порт 443 отдаёт файл через nginx (перезагрузка не нужна). UDP-порт Hysteria2 держит копию # страницы внутри подключения — её надо обновить, иначе ответы разойдутся. if kl_have kit; then if [[ -f /etc/kit/kl-protos ]] && ! grep -qE '(^|[ ,])hy2([ ,]|$)' /etc/kit/kl-protos; then kl_ok "Hysteria2 в этой установке не ставился — синхронизировать нечего" elif kit net masq on >/dev/null 2>&1; then kl_ok "Hysteria2 отвечает той же страницей" else kl_warn "Hysteria2 не синхронизирован (может, его нет в этой установке) — проверьте: kit net masq on" fi fi kl_ok "посмотреть снаружи: curl -k https:/// ; состояние: kit local site --show, kit check" } kit_local_cmd() { case ${1:-help} in help | -h | --help) kit_local_help ;; version | -v) kit_local_version ;; channel) kit_local_channel ;; seams) kit_local_seams ;; masking | mask) kit_local_masking ;; ssh | ssh-port) shift; kit_local_ssh "$@" ;; ping | icmp) shift; kit_local_ping "$@" ;; passwd | pass) shift; kit_local_passwd "$@" ;; upgrades | auto) shift; kit_local_upgrades "$@" ;; hardening | hard) shift; kit_local_hardening "$@" ;; uninstall | remove) shift; kit_local_uninstall "$@" ;; links | link) shift; kit_local_links "$@" ;; ports) shift; kit_local_ports "$@" ;; site | stub | stub-site) shift; kit_local_site "$@" ;; *) printf 'Неизвестная команда форка: %s\n' "$1" >&2 kit_local_help >&2 return 2 ;; esac } # Точка входа: вызывается из kit.sh с аргументами командной строки. # Если команда не наша — возвращаем 0, и kit работает ровно как раньше. # ---------------------------------------------------------------- снос установки целиком # Убираем всё, что появляется после установки: панель 3X-UI и службу x-ui, ядро Xray, службы и файлы # kit (kit-sub, автообновление, cron), конфиги nginx и сайт-заглушку, сертификаты, правила ufw для 80/443, # наши правки системы (правило ICMP, обновления безопасности), наши бэкапы и файл с данными панели. # # ЧЕГО НЕ ТРОГАЕМ: SSH — ни порт, ни конфиги, ни правила ufw для него, ни наш ssh-дроп-ин. Иначе легко # остаться без доступа к серверу. Если SSH-порт когда-то менялся, верните его сами: kit local ssh 22 # (и закрыть лишние: kit local ssh --close-old 22). # # kit local uninstall спросит подтверждение словом «удалить» # kit local uninstall --dry-run только покажет, что будет удалено kit_local_ports() { # [--close-extra] — что открыто в ufw, что реально нужно и как закрыть лишнее local close=no a for a in "$@"; do case $a in --close-extra) close=yes ;; --help | -h) printf 'kit local ports [--close-extra] — открытые порты, нужные и лишние\n'; return 0 ;; *) kl_die "kit local ports [--close-extra]"; return 1 ;; esac done command -v ufw >/dev/null 2>&1 || { kl_warn "ufw не установлен — порты смотреть нечем"; return 0; } local -a opened=() needed=() extra=() mapfile -t opened < <(ufw status 2>/dev/null | awk '$2 == "ALLOW" { print $1 }' | sort -u) # Нужные порты: те, что слушают наши подключения (x-ui и ядро Xray), плюс SSH. mapfile -t needed < <({ ss -tlnp 2>/dev/null; ss -ulnp 2>/dev/null; } \ | awk '/x-ui|xray/ { print $4 }' | sed 's/.*://' | sort -un; ss -tlnp 2>/dev/null | awk '/sshd/ { print $4 }' | sed 's/.*://' | sort -un) local p q keep for p in "${opened[@]}"; do keep=no case $p in 80/tcp | 443/tcp) keep=yes ;; esac # 443 — работа, 80 — продление сертификата for q in "${needed[@]}"; do [[ $p == "$q/tcp" || $p == "$q/udp" || $p == "$q" ]] && keep=yes; done [[ $keep == yes ]] && continue extra+=("$p") done printf ' открыто в ufw: %s\n' "$(printf '%s ' "${opened[@]}")" if ((${#needed[@]})); then printf ' нужны: %s\n' "$(printf '%s/tcp %s/udp ' "${needed[@]}" "${needed[@]}")" else printf ' нужны: (не определил — смотрите kit check)\n'; fi if ((${#extra[@]})); then printf ' лишние: %s\n' "$(printf '%s ' "${extra[@]}")" printf ' закрыть лишние: kit local ports --close-extra\n' if [[ $close == yes ]]; then for p in "${extra[@]}"; do ufw --force delete allow "$p" >/dev/null 2>&1 && printf ' закрыл %s\n' "$p"; done fi else printf ' лишних нет\n' fi } kit_local_links() { # [имя] — подписка, QR-код и ссылки по протоколам (то же, что «kit → Пользователи → Показать ссылки») local u=${1:-} now if [[ -z $u ]]; then u=$(kit user list 2>/dev/null | awk 'NR > 1 && NF { print $1; exit }') u=${u:-admin} fi printf ' пользователь: %s\n' "$u" now=$(tr '\n' ' ' /dev/null || true) # Ссылки на каждый протокол — только если протоколов больше одного; при одном достаточно подписки. if [[ $(wc -w <<<"$now") -gt 1 ]]; then kit user link "$u" --all; else kit user link "$u"; fi } kit_local_uninstall() { # Наш шов: логику удаления отдаём авторской команде «kit uninstall». Она снимает и сам kit, чтобы на # сервере не осталось следов, поэтому своя реализация здесь больше не нужна — команда оставлена # синонимом, чтобы привычное «kit local uninstall» продолжало работать. local a=() x for x in "$@"; do case $x in --dry-run | --yes | --no-backup) a+=("$x") ;; --help | -h) printf 'kit local uninstall [--dry-run] [--yes] [--no-backup] — то же, что kit uninstall\n' return 0 ;; *) kl_die "kit local uninstall [--dry-run] [--yes] [--no-backup]"; return 1 ;; esac done [[ -x /usr/local/bin/kit ]] || { kl_die "не нашёл /usr/local/bin/kit — удалить вручную: kit uninstall"; return 1; } printf ' удаляю по авторской логике: /usr/local/bin/kit uninstall %s\n' "${a[*]}" exec /usr/local/bin/kit uninstall "${a[@]}" } # ---------------------------------------------------------------- полная справка `kit help` # Категории и все команды: сначала авторские, затем наши. Если автор добавит команду, а справка # про неё не знает — сборку релиза остановит проверка tools/check-help.sh. kit_help_all() { cat < [--gb N] [--days N] [--devices N] создать (пусто/0 — без ограничения) kit user list список пользователей kit user link <имя> [--all | --amnezia | --telegram] подписка с QR-кодом и ссылки kit user limit <имя> --gb N --days N --devices N лимиты трафика, срока, устройств kit user on <имя> включить пользователя kit user off <имя> выключить пользователя kit user del <имя> удалить kit user enforce применить лимиты сейчас kit user vision <имя> [on|off] поток xtls-rprx-vision у REALITY Подключения и порты kit net состояние подключений kit net on <имя> включить подключение kit net off <имя> выключить подключение kit net port <имя> <порт> перевести подключение на другой порт kit net vision on|off Vision у всех REALITY сразу kit net split on|off|check|apply разделение трафика по правилам (split) kit port какие порты слушают подключения kit port set <имя> <порт> сменить порт подключения kit reality add [порт] добавить второй REALITY Маскировка и сайт kit net site [сайт] сменить сайт маскировки (без аргумента — подобрать соседа по подсети) kit net fp <отпечаток> отпечаток TLS (chrome, firefox, safari, …) kit sni какие сайты маскировки у подключений kit sni rotate сменить сайты маскировки у всех подключений kit local masking состояние маскировки (у REALITY должен быть свой домен) kit local site [--list | N | --show] сайт-заглушка: случайный ИТ-сайт, список, конкретный, какой стоит Проверка и починка kit local links [имя] подписка, QR-код и ссылки по протоколам (по умолчанию — первый пользователь) kit local ports [--close-extra] открытые порты: нужные, лишние; --close-extra закрывает лишние kit local uninstall то же, что kit uninstall: снести установку целиком и сам kit; SSH не трогает kit check [--deep | --fix] проверка сервера (--deep — ходит наружу как клиент) kit fix [--dry-run] безопасные починки: время, службы, конфиги kit local hardening сводка по защите и что ещё можно включить Обслуживание и обновление kit update [--panel | --force | --auto | --manual] обновить сборку, панель, автообновление kit panel update обновить панель 3X-UI kit backup копия сервера: панель, сертификаты, настройки kit net masq on|off ответ Hysteria2 страницей сайта kit net dns on|off DoH в подписках Защита сервера (наши команды) kit local ssh <порт> | --status | --close-old <порт> добавить порт SSH, какие слушают, закрыть прежние kit local ping off | on | status ответ на ping (правилом ufw) kit local passwd [--generate] [пользователь] сменить пароль (в логи не попадает) kit local upgrades on | off | status обновления безопасности Общие правила * у наших команд есть --dry-run: показывает план и ничего не меняет * подробная справка по нашему слою: kit local help * копии изменённых файлов: /root/kit-local-backup-<дата>/ EOF } kit_local_dispatch() { case ${1:-} in local) shift kit_local_cmd "$@" exit $? ;; help | -h | --help) # Полная справка: авторские команды + наши, по категориям. kit_help_all exit 0 ;; *) return 0 ;; esac }