v1.2.6: 10 ИТ-сайтов-заглушек вместо авторских + умолчание протоколов REALITY и AWG-3.1

This commit is contained in:
2026-10-12 20:30:14 +03:00
parent 1fdc4945f1
commit 445c066791
4 changed files with 466 additions and 11 deletions
+8
View File
@@ -5,6 +5,14 @@
## v1.2.6: вопросы при установке ## v1.2.6: вопросы при установке
* Набор по умолчанию — **REALITY + AmneziaWG-3.1** (самый совместимый минимум); ответ «все»
ставит полный набор автора. Умолчание задаётся в одном месте (`DEFAULT_PROTOS`).
* **Сайт-заглушка переделан на ИТ-тематику:** десять правдоподобных сайтов (хостинг, DevOps,
мониторинг, разработка, аудит, ИТ-поддержка, колокейшн, заметки про Linux, бэкапы, документация)
вместо авторских «кофеен и пекарен». Ставится случайный, конкретный — `kit local site 7`,
список — `kit local site --list`. Команда обновляет и файл для nginx, и копию страницы внутри
подключения Hysteria2 (`kit net masq on`): если менять файл вручную, 443/tcp и UDP-порт начнут
отвечать разными страницами — это заметно снаружи.
* Обёртка спрашивает, **какие протоколы ставить** (нумерованный список, `1,3,11` или «все», * Обёртка спрашивает, **какие протоколы ставить** (нумерованный список, `1,3,11` или «все»,
Enter — набор автора): невыбранные протоколы не создаются вообще — ни подключения в панели, Enter — набор автора): невыбранные протоколы не создаются вообще — ни подключения в панели,
ни записей в nginx, ни ссылок в подписке. Список и умолчание взяты из самого установщика ни записей в nginx, ни ссылок в подписке. Список и умолчание взяты из самого установщика
+5 -2
View File
@@ -9,7 +9,7 @@
1. Если нужен свой домен для маскировки — заранее направьте его A-запись на IP сервера и освободите 1. Если нужен свой домен для маскировки — заранее направьте его A-запись на IP сервера и освободите
порт 80 (иначе Let's Encrypt не выпустит сертификат). Без домена этот шаг пропускается. порт 80 (иначе Let's Encrypt не выпустит сертификат). Без домена этот шаг пропускается.
2. Одна команда на сервере (от root) — **дальше скрипт спросит сам**: какие протоколы ставить 2. Одна команда на сервере (от root) — **дальше скрипт спросит сам**: какие протоколы ставить
(нумерованный список: `1,3,11` или «все», Enter — набор автора), какую маскировку и домен, (Enter — REALITY + AmneziaWG-3.1, «все» — набор автора), какую маскировку и домен,
закрывать ли ответ на ping, включать ли автообновления, менять ли порт SSH (Enter — оставить закрывать ли ответ на ping, включать ли автообновления, менять ли порт SSH (Enter — оставить
текущий, число — перевести на него), менять ли пароль root: текущий, число — перевести на него), менять ли пароль root:
@@ -41,7 +41,8 @@ bash <(curl -fsSL …/scripts/3x-ui-local.sh) --dry-run --domain vpn.example.com
| Флаг | Что делает | | Флаг | Что делает |
|---|---| |---|---|
| `--protocols список` | какие протоколы ставить: `all`, `minimal` или через запятую — `reality,reality2,hy2,xhttp,ws,trojan,vmess,ss,tuic,wg,awg,awg3,mtproto`. Невыбранные не создаются вообще. Без флага — вопрос | | `--protocols список` | какие протоколы ставить: `all`, `minimal` или через запятую — `reality,reality2,hy2,xhttp,ws,trojan,vmess,ss,tuic,wg,awg,awg3,mtproto`. Невыбранные не создаются вообще. Без флага — вопрос, по умолчанию предлагаются **REALITY + AmneziaWG-3.1** (ответ «все» — набор автора) |
| `--site N\|random\|off` | сайт-заглушка: конкретный из нашего набора (10 ИТ-сайтов), случайный (по умолчанию) или не трогать авторскую страницу (то же — `--no-site`) |
| `--yes` | не задавать вопросы (ping закрывается, автообновления включаются, порт SSH и пароль не трогаются) | | `--yes` | не задавать вопросы (ping закрывается, автообновления включаются, порт SSH и пароль не трогаются) |
| `--harden` | закрыть ответ на ping и включить автообновления без вопросов | | `--harden` | закрыть ответ на ping и включить автообновления без вопросов |
| `--domain домен` | свой домен для маскировки: сайт и REALITY маскируются под него (A-запись на сервер, свободный 80) | | `--domain домен` | свой домен для маскировки: сайт и REALITY маскируются под него (A-запись на сервер, свободный 80) |
@@ -88,6 +89,8 @@ kit local hardening # сводка по защите сервер
| Команда | Что делает | | Команда | Что делает |
|---|---| |---|---|
| `kit local site` | поставить случайный сайт-заглушку из нашего набора (10 ИТ-сайтов) и синхронизировать ответ Hysteria2 |
| `kit local site --list` / `kit local site 7` / `--show` | список сайтов с номерами / поставить конкретный / какой стоит сейчас |
| `kit local ssh 2222` | добавить порт SSH (прежние продолжают работать; порт сначала открывается в ufw, конфиг проверяется через `sshd -t`) | | `kit local ssh 2222` | добавить порт SSH (прежние продолжают работать; порт сначала открывается в ufw, конфиг проверяется через `sshd -t`) |
| `kit local ssh --status` | какие порты SSH слушаются сейчас | | `kit local ssh --status` | какие порты SSH слушаются сейчас |
| `kit local ssh --close-old 2222` | закрыть прежние порты после того, как новый проверен из второго окна | | `kit local ssh --close-old 2222` | закрыть прежние порты после того, как новый проверен из второго окна |
+45 -8
View File
@@ -9,10 +9,12 @@
# а можно ничего не передавать — тогда он задаст вопросы сам (в том числе про домен); # а можно ничего не передавать — тогда он задаст вопросы сам (в том числе про домен);
# 4) спрашивает, какие протоколы ставить: невыбранные не создаются вообще — ни подключения в панели, # 4) спрашивает, какие протоколы ставить: невыбранные не создаются вообще — ни подключения в панели,
# ни записей в nginx, ни ссылок в подписке (за это отвечает флаг автора --protocols); # ни записей в nginx, ни ссылок в подписке (за это отвечает флаг автора --protocols);
# 5) спрашивает и делает то, чего у автора нет: закрыть ответ на ping, включить обновления # 5) ставит сайт-заглушку: случайный правдоподобный ИТ-сайт из нашего набора (10 штук;
# --site N — конкретный, --no-site — не трогать авторскую страницу);
# 6) спрашивает и делает то, чего у автора нет: закрыть ответ на ping, включить обновления
# безопасности, перевести SSH на другой порт, сменить пароль root (поведение из StressOzz/vps-setup, # безопасности, перевести SSH на другой порт, сменить пароль root (поведение из StressOzz/vps-setup,
# но безопаснее); # но безопаснее);
# 6) в конце: kit fix (время и безопасные починки), kit check, kit local hardening. # 7) в конце: kit fix (время и безопасные починки), kit check, kit local hardening.
# #
# Запуск: # Запуск:
# # одна команда, дальше — вопросы: # # одна команда, дальше — вопросы:
@@ -73,6 +75,8 @@ usage() {
--no-harden не трогать ping и автообновления --no-harden не трогать ping и автообновления
--ssh-port N перевести SSH на порт N (прежний закроется после проверки входа) --ssh-port N перевести SSH на порт N (прежний закроется после проверки входа)
--yes ни о чём не спрашивать (для автоматизации) --yes ни о чём не спрашивать (для автоматизации)
--site N|random|off сайт-заглушка: конкретный из нашего набора, случайный (по умолчанию) или
не трогать авторскую страницу (то же: --no-site)
--no-time не запускать kit fix (время и безопасные починки) --no-time не запускать kit fix (время и безопасные починки)
--no-check не запускать в конце kit check --no-check не запускать в конце kit check
--verify-only только проверить релиз (подпись и SHA256) и выйти --verify-only только проверить релиз (подпись и SHA256) и выйти
@@ -84,6 +88,7 @@ EOF
# ------------------------------------------------------------------ разбор флагов # ------------------------------------------------------------------ разбор флагов
INSTALLER_ARGS=(); harden=auto; ssh_port=""; fix_time=yes; do_check=yes; dry=no; verify_only=no; assume_yes=no INSTALLER_ARGS=(); harden=auto; ssh_port=""; fix_time=yes; do_check=yes; dry=no; verify_only=no; assume_yes=no
site=random # заглушка: случайный сайт из нашего набора (off — не трогать)
while (($#)); do while (($#)); do
case $1 in case $1 in
--harden) harden=yes ;; --harden) harden=yes ;;
@@ -91,6 +96,9 @@ while (($#)); do
--ssh-port) ssh_port=${2:-}; shift || die "--ssh-port: нужен номер порта" ;; --ssh-port) ssh_port=${2:-}; shift || die "--ssh-port: нужен номер порта" ;;
--ssh-port=*) ssh_port=${1#*=} ;; --ssh-port=*) ssh_port=${1#*=} ;;
--yes | -y) assume_yes=yes ;; --yes | -y) assume_yes=yes ;;
--site) site=${2:-random}; shift || die "--site: нужен номер сайта (1..10), random или off" ;;
--site=*) site=${1#*=} ;;
--no-site) site=off ;;
--no-time) fix_time=no ;; --no-time) fix_time=no ;;
--no-check) do_check=no ;; --no-check) do_check=no ;;
--verify-only) verify_only=yes ;; --verify-only) verify_only=yes ;;
@@ -102,6 +110,11 @@ while (($#)); do
done done
[[ -z $ssh_port ]] || { [[ $ssh_port =~ ^[0-9]{1,5}$ ]] && ((10#$ssh_port >= 1024 && 10#$ssh_port <= 65535)); } \ [[ -z $ssh_port ]] || { [[ $ssh_port =~ ^[0-9]{1,5}$ ]] && ((10#$ssh_port >= 1024 && 10#$ssh_port <= 65535)); } \
|| die "--ssh-port: нужно число 1024–65535, например 2222" || die "--ssh-port: нужно число 1024–65535, например 2222"
if [[ $site != off && $site != random ]]; then
# 10 — число сайтов в наборе (scripts/kit-local.sh, KL_SITE_TITLES); добавите сайт — поднимите границу
[[ $site =~ ^[0-9]{1,2}$ ]] && ((10#$site >= 1 && 10#$site <= 10)) \
|| die "--site: random, off или номер 1..10 (список: kit local site --list)"
fi
# Если --protocols задан флагом — не спрашиваем и не дублируем его. # Если --protocols задан флагом — не спрашиваем и не дублируем его.
proto_flag="" proto_flag=""
@@ -124,11 +137,16 @@ if [[ $dry == yes ]]; then
elif [[ $harden == yes ]]; then pre "защита: закрыть ping и включить автообновления (--harden)" elif [[ $harden == yes ]]; then pre "защита: закрыть ping и включить автообновления (--harden)"
else pre "защита: пропускаю (--no-harden)"; fi else pre "защита: пропускаю (--no-harden)"; fi
if [[ -n $proto_flag ]]; then pre "протоколы: ${proto_flag} (задано флагом)" if [[ -n $proto_flag ]]; then pre "протоколы: ${proto_flag} (задано флагом)"
elif interactive; then pre "протоколы: спрошу (Enter — всё, кроме WireGuard и REALITY-2)" elif interactive; then pre "протоколы: спрошу (Enter — REALITY + AmneziaWG-3.1, «все» — набор автора)"
else pre "протоколы: всё, кроме WireGuard и REALITY-2"; fi else pre "протоколы: REALITY + AmneziaWG-3.1 (без терминала вопроса нет)"; fi
if [[ -n $ssh_port ]]; then pre "порт SSH: перевожу на $ssh_port (прежний — после проверки входа)" if [[ -n $ssh_port ]]; then pre "порт SSH: перевожу на $ssh_port (прежний — после проверки входа)"
elif interactive; then pre "порт SSH: спрошу (Enter — оставить текущий)"; fi elif interactive; then pre "порт SSH: спрошу (Enter — оставить текущий)"; fi
if interactive; then pre "пароль root: спрошу (по умолчанию — не менять)"; fi if interactive; then pre "пароль root: спрошу (по умолчанию — не менять)"; fi
case $site in
off) pre "заглушка: оставляю авторскую страницу (--no-site)" ;;
random) pre "заглушка: случайный ИТ-сайт из нашего набора (10 штук)" ;;
*) pre "заглушка: сайт №$site из нашего набора (список: kit local site --list)" ;;
esac
[[ $fix_time == yes ]] && pre "после: kit fix (время, безопасные починки)" [[ $fix_time == yes ]] && pre "после: kit fix (время, безопасные починки)"
[[ $do_check == yes ]] && pre "проверка: kit check + kit local hardening" [[ $do_check == yes ]] && pre "проверка: kit check + kit local hardening"
echo echo
@@ -197,7 +215,10 @@ PROTO_HINT=(
"AmneziaWG-3.1 AmneziaWG с защитой заголовков: AmneziaVPN" "AmneziaWG-3.1 AmneziaWG с защитой заголовков: AmneziaVPN"
"MTProto прокси для Telegram" "MTProto прокси для Telegram"
) )
DEFAULT_PROTOS=(reality hy2 xhttp ws trojan vmess ss tuic awg awg3 mtproto) # Наш набор по умолчанию: VLESS REALITY и AmneziaWG-3.1 — самый совместимый минимум.
# Набор автора (флаг --protocols all) доступен ответом «все».
AUTHOR_PROTOS=(reality hy2 xhttp ws trojan vmess ss tuic awg awg3 mtproto)
DEFAULT_PROTOS=(reality awg3)
proto_menu() { proto_menu() {
local i n mark local i n mark
@@ -207,7 +228,8 @@ proto_menu() {
[[ " ${DEFAULT_PROTOS[*]} " == *" ${ALL_PROTOS[i]} "* ]] && mark="*" [[ " ${DEFAULT_PROTOS[*]} " == *" ${ALL_PROTOS[i]} "* ]] && mark="*"
printf ' %s %2d. %s\n' "$mark" "$n" "${PROTO_HINT[i]}" >&2 printf ' %s %2d. %s\n' "$mark" "$n" "${PROTO_HINT[i]}" >&2
done done
printf ' %s\n' "${D}[* — набор по умолчанию (всё, кроме WireGuard и REALITY-2). Невыбранные протоколы не создаются вообще.]${N}" >&2 printf ' %s\n' "${D}[* — по умолчанию: REALITY + AmneziaWG-3.1. «все» — набор автора (всё, кроме WireGuard).${N}" >&2
printf ' %s\n' "${D}[Невыбранные протоколы не создаются вообще: ни подключения, ни записей в nginx, ни ссылок в подписке.]${N}" >&2
} }
proto_parse() { # ответ → выбранные имена через запятую; код 1, если не разобрал proto_parse() { # ответ → выбранные имена через запятую; код 1, если не разобрал
@@ -215,7 +237,7 @@ proto_parse() { # ответ → выбранные имена через зап
ans=${ans//[[:space:]]/,} # «1 3 11» и «1,3,11» — одно и то же ans=${ans//[[:space:]]/,} # «1 3 11» и «1,3,11» — одно и то же
[[ -n $ans ]] || { printf '%s' "$(IFS=,; echo "${DEFAULT_PROTOS[*]}")"; return 0; } [[ -n $ans ]] || { printf '%s' "$(IFS=,; echo "${DEFAULT_PROTOS[*]}")"; return 0; }
case $ans in case $ans in
all | все | всё) printf '%s' "$(IFS=,; echo "${DEFAULT_PROTOS[*]}")"; return 0 ;; all | все | всё) printf '%s' "$(IFS=,; echo "${AUTHOR_PROTOS[*]}")"; return 0 ;;
minimal | минимум) printf '%s' "reality"; return 0 ;; minimal | минимум) printf '%s' "reality"; return 0 ;;
esac esac
local IFS=',' local IFS=','
@@ -238,7 +260,7 @@ ask_protocols() { # печатает итоговый список через з
interactive || { printf '%s' "$(IFS=,; echo "${DEFAULT_PROTOS[*]}")"; return 0; } interactive || { printf '%s' "$(IFS=,; echo "${DEFAULT_PROTOS[*]}")"; return 0; }
proto_menu proto_menu
for i in 1 2 3; do for i in 1 2 3; do
printf '\n%sНомера через запятую (например 1,3,11) или «все»; Enter — набор по умолчанию: %s' "$B" "$N" >&2 printf '\n%sНомера через запятую (например 1,12) или «все»; Enter — REALITY + AmneziaWG-3.1: %s' "$B" "$N" >&2
ans="" ans=""
read -r -t 300 ans <"$ASK_SRC" 2>/dev/null || true read -r -t 300 ans <"$ASK_SRC" 2>/dev/null || true
if picked=$(proto_parse "$ans"); then printf '%s' "$picked"; return 0; fi if picked=$(proto_parse "$ans"); then printf '%s' "$picked"; return 0; fi
@@ -281,6 +303,21 @@ ask_yes() { # "вопрос" y|n — по умолчанию
[[ $ans =~ ^(y|yes|д|да)$ ]] [[ $ans =~ ^(y|yes|д|да)$ ]]
} }
# Сайт-заглушка: у автора это случайные «кофейни и пекарни», у нас — правдоподобный ИТ-сайт.
# Меняем не файл вручную, а командой форка: она же обновляет копию страницы в ответе Hysteria2,
# иначе 443/tcp и UDP-порт начнут отвечать разными страницами (это видно снаружи).
case $site in
off) say "заглушку не трогаю (--no-site): остаётся страница автора" ;;
random)
say "ставлю сайт-заглушку: случайный ИТ-сайт из нашего набора"
kit local site || warn "не удалось поставить заглушку — позже: kit local site"
;;
*)
say "ставлю сайт-заглушку №$site"
kit local site "$site" || warn "не удалось поставить заглушку — позже: kit local site $site"
;;
esac
if [[ $fix_time == yes ]]; then if [[ $fix_time == yes ]]; then
say "kit fix: синхронизация времени и безопасные починки (авторская команда, ничего необратимого)" say "kit fix: синхронизация времени и безопасные починки (авторская команда, ничего необратимого)"
kit fix || warn "kit fix завершился с замечаниями — посмотрите вывод" kit fix || warn "kit fix завершился с замечаниями — посмотрите вывод"
+407
View File
@@ -74,6 +74,12 @@ kit_local_help() {
kit local masking состояние маскировки (у REALITY должен быть свой домен) kit local masking состояние маскировки (у REALITY должен быть свой домен)
kit local hardening сводка по защите сервера и что ещё можно включить kit local hardening сводка по защите сервера и что ещё можно включить
Заглушка (сайт на 443 и ответ Hysteria2 — одна и та же страница)
kit local site поставить случайный сайт из нашего набора (10 штук)
kit local site --list список сайтов с номерами
kit local site 7 поставить конкретный
kit local site --show какой сайт стоит сейчас
Защита сервера (поведение взято из StressOzz/vps-setup, сделано безопаснее) Защита сервера (поведение взято из StressOzz/vps-setup, сделано безопаснее)
kit local ssh 2222 добавить порт SSH (прежние продолжают работать) kit local ssh 2222 добавить порт SSH (прежние продолжают работать)
kit local ssh --status какие порты SSH слушаются сейчас kit local ssh --status какие порты SSH слушаются сейчас
@@ -452,6 +458,406 @@ EOF
return 0 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=(
"Nordhost — VPS and cloud hosting"
"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=(
"VPS-хостинг: тарифы, регионы, платформа"
"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}
@media(max-width:600px){.hero{padding:44px 0 28px}.hero h1{font-size:29px}}'
kl_site_open() { # заголовок описание бренд «навигация как HTML»
cat <<HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>$1</title>
<meta name="description" content="$2">
<meta name="generator" content="static">
<style>$KL_SITE_CSS</style>
</head>
<body>
<header><div class="wrap"><a class="logo" href="/">$3</a><nav>$4</nav></div></header>
HTML
}
kl_site_close() { # бренд
cat <<HTML
<footer><div class="wrap"><p>© $(date +%Y) $1 · <a href="#">Imprint</a> · <a href="#">Privacy</a> · <a href="#">Status</a></p></div></footer>
</body>
</html>
HTML
}
kl_site_html() { # номер (с нуля) → страница на stdout
case $1 in
0) # хостинг
kl_site_open "Nordhost — VPS and cloud hosting" "Virtual servers on NVMe storage in three European regions. Hourly billing, snapshots, private networks and a full API." "Nordhost" '<a href="#plans">Plans</a><a href="#network">Network</a><a href="#docs">Docs</a>'
cat <<'HTML'
<main>
<section class="hero"><div class="wrap">
<h1>Virtual servers that stay out of the way</h1>
<p>KVM instances in Frankfurt, Amsterdam and Zurich. Hourly billing, snapshots, private networking, and an API for everything the control panel can do.</p>
<a class="btn" href="#plans">See plans</a>
</div></section>
<section id="plans" class="wrap cards">
<div class="card"><h3>Starter — €4/mo</h3><p>1 vCPU, 2 GB RAM, 40 GB NVMe, 20 TB traffic.</p></div>
<div class="card"><h3>Standard — €9/mo</h3><p>2 vCPU, 4 GB RAM, 80 GB NVMe, 40 TB traffic.</p></div>
<div class="card"><h3>Compute — €18/mo</h3><p>4 vCPU, 8 GB RAM, 160 GB NVMe, 100 TB traffic.</p></div>
</section>
<section id="network" class="wrap prose">
<h2>Network</h2>
<p>Every region is connected with redundant 10 GbE uplinks. Traffic inside a region is free, between regions it is billed at cost. IPv6 is included, IPv4 on request.</p>
<h2>Platform</h2>
<ul>
<li>Snapshots, weekly backups and one-click rebuilds</li>
<li>Private networks, floating IPs and firewalls</li>
<li>cloud-init, Terraform provider and REST API</li>
<li>Two-factor sign-in and audit log</li>
</ul>
</section>
</main>
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" '<a href="#work">Work</a><a href="#approach">Approach</a><a href="#about">About</a>'
cat <<'HTML'
<main>
<section class="hero"><div class="wrap">
<h1>Ship on Fridays without holding your breath</h1>
<p>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.</p>
<a class="btn" href="#work">What we do</a>
</div></section>
<section id="work" class="wrap cards">
<div class="card"><h3>CI/CD</h3><p>Build pipelines, artefact registries, staging environments and one-command rollbacks.</p></div>
<div class="card"><h3>Infrastructure</h3><p>Terraform and Ansible for servers and cloud, with reviews and tests in the pipeline.</p></div>
<div class="card"><h3>Observability</h3><p>Metrics, logs and traces wired together, with alerts that people actually trust.</p></div>
</section>
<section id="approach" class="wrap prose">
<h2>How we work</h2>
<p>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.</p>
<table>
<tr><th>Week</th><th>What happens</th></tr>
<tr><td>1–2</td><td>Audit of the current setup, a written report with priorities</td></tr>
<tr><td>3–6</td><td>Pipelines and infrastructure, pairing with your engineers</td></tr>
<tr><td>7–8</td><td>Monitoring, runbooks and a handover session</td></tr>
</table>
</section>
</main>
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" '<a href="#checks">Checks</a><a href="#alerts">Alerts</a><a href="#pricing">Pricing</a>'
cat <<'HTML'
<main>
<section class="hero"><div class="wrap">
<h1>Know about the outage before your users do</h1>
<p>HTTP, TCP, ICMP and TLS checks from six locations, host metrics through a lightweight agent, and alerts that reach you in seconds.</p>
<a class="btn" href="#checks">See checks</a>
</div></section>
<section id="checks" class="wrap cards">
<div class="card"><h3>Availability</h3><p>HTTP and TCP checks with keyword matching, redirects and status codes.</p></div>
<div class="card"><h3>Resources</h3><p>CPU, memory, disk and network of every host on one page.</p></div>
<div class="card"><h3>Certificates</h3><p>Expiry warnings 30, 14 and 3 days ahead, with a list of what needs renewing.</p></div>
</section>
<section id="alerts" class="wrap prose">
<h2>Alerting</h2>
<p>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.</p>
<h2>Maybe you do not need us yet</h2>
<p>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.</p>
</section>
</main>
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" '<a href="#services">Services</a><a href="#stack">Stack</a><a href="#contact">Contact</a>'
cat <<'HTML'
<main>
<section class="hero"><div class="wrap">
<h1>Software for the part of the business nobody sells</h1>
<p>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.</p>
<a class="btn" href="#services">Services</a>
</div></section>
<section id="services" class="wrap cards">
<div class="card"><h3>Web services</h3><p>APIs and admin panels in Python or Go, deployed to your infrastructure.</p></div>
<div class="card"><h3>Integrations</h3><p>Accounting, warehouses, CRM and everything that has an API or a database.</p></div>
<div class="card"><h3>Rescue work</h3><p>Taking over an unfinished project: audit, plan, tidy up, finish.</p></div>
</section>
<section id="stack" class="wrap prose">
<h2>Stack</h2>
<p>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.</p>
<h2>Team</h2>
<p>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.</p>
</section>
</main>
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" '<a href="#services">Services</a><a href="#process">Process</a><a href="#contact">Contact</a>'
cat <<'HTML'
<main>
<section class="hero"><div class="wrap">
<h1>Find the problems before somebody else does</h1>
<p>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.</p>
<a class="btn" href="#services">Services</a>
</div></section>
<section id="services" class="wrap cards">
<div class="card"><h3>External test</h3><p>Everything reachable from the internet: services, TLS, DNS, mail.</p></div>
<div class="card"><h3>Internal test</h3><p>What an attacker does after getting a foothold in an office or a VPN.</p></div>
<div class="card"><h3>Review</h3><p>Settings, backups, access rights and monitoring — usually the cheapest findings.</p></div>
</section>
<section id="process" class="wrap prose">
<h2>Process</h2>
<p>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.</p>
<h2>Report</h2>
<p>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.</p>
</section>
</main>
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" '<a href="#services">Services</a><a href="#support">Support</a><a href="#contact">Contact</a>'
cat <<'HTML'
<main>
<section class="hero"><div class="wrap">
<h1>An IT department, without hiring one</h1>
<p>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.</p>
<a class="btn" href="#services">What is included</a>
</div></section>
<section id="services" class="wrap cards">
<div class="card"><h3>Servers</h3><p>Updates, monitoring, disk space and certificate renewal handled for you.</p></div>
<div class="card"><h3>Backups</h3><p>Nightly copies, offsite storage and a restore test twice a year.</p></div>
<div class="card"><h3>Workplaces</h3><p>Accounts, laptops, printers and the printer driver that never works.</p></div>
</section>
<section id="support" class="wrap prose">
<h2>Support</h2>
<p>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.</p>
<p>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.</p>
</section>
</main>
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" '<a href="#colo">Colocation</a><a href="#transit">Transit</a><a href="#contact">Contact</a>'
cat <<'HTML'
<main>
<section class="hero"><div class="wrap">
<h1>Racks, cross-connects and transit</h1>
<p>Quarter, half and full racks in Frankfurt and Amsterdam, with remote hands, redundant power and a choice of transit providers.</p>
<a class="btn" href="#colo">Colocation</a>
</div></section>
<section id="colo" class="wrap cards">
<div class="card"><h3>Quarter rack</h3><p>10 U, 2 kW, 1 Gbps port and one IPv4 prefix.</p></div>
<div class="card"><h3>Half rack</h3><p>22 U, 4 kW and a redundant 10 Gbps pair.</p></div>
<div class="card"><h3>Private cage</h3><p>From eight racks, with your own access control and monitoring.</p></div>
</section>
<section id="transit" class="wrap prose">
<h2>Transit and peering</h2>
<p>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.</p>
<table>
<tr><th>Port</th><th>Committed rate</th></tr>
<tr><td>1 Gbps</td><td>from 50 Mbps</td></tr>
<tr><td>10 Gbps</td><td>from 1 Gbps</td></tr>
<tr><td>2×10 Gbps</td><td>from 2 Gbps, redundant</td></tr>
</table>
</section>
</main>
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" '<a href="#posts">Posts</a><a href="#about">About</a>'
cat <<'HTML'
<main>
<section class="hero"><div class="wrap">
<h1>Notes from a server room</h1>
<p>Things I worked out once and do not want to work out again: nginx, systemd, ZFS, routing and the odd shell trick.</p>
<a class="btn" href="#posts">Read the posts</a>
</div></section>
<section id="posts" class="wrap prose">
<h2>Replacing a certificate without dropping connections</h2>
<p>Reload, not restart. <code>systemctl reload nginx</code> keeps the listeners open and swaps the configuration, so long downloads and websockets survive. A restart does not.</p>
<h2>Finding what filled the disk</h2>
<p><code>du -xh --max-depth=1 / | sort -h</code> first, then follow the biggest branch. Journals under <code>/var/log/journal</code> are a common answer; <code>journalctl --vacuum-size=200M</code> trims them.</p>
<h2>Checking that a port is really listening</h2>
<p><code>ss -ltnp</code> 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.</p>
<h2>Backup is not a copy</h2>
<p>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.</p>
</section>
</main>
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" '<a href="#policy">Policy</a><a href="#restore">Restore</a><a href="#contact">Contact</a>'
cat <<'HTML'
<main>
<section class="hero"><div class="wrap">
<h1>A backup you have never restored is a rumour</h1>
<p>We copy your servers and databases to a separate location, keep versions, and prove once a quarter that a restore actually works.</p>
<a class="btn" href="#policy">How it works</a>
</div></section>
<section id="policy" class="wrap cards">
<div class="card"><h3>Nightly copies</h3><p>Files and databases, encrypted before they leave the server.</p></div>
<div class="card"><h3>Versions</h3><p>Hourly for a day, daily for a month, monthly for a year.</p></div>
<div class="card"><h3>Restore test</h3><p>A quarterly restore into a clean machine, with a short report.</p></div>
</section>
<section id="restore" class="wrap prose">
<h2>Recovery</h2>
<p>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.</p>
<p>Encryption keys are kept by you as well as by us, so that neither side alone can read the copies.</p>
</section>
</main>
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" '<a href="#install">Install</a><a href="#config">Config</a><a href="#reference">Reference</a>'
cat <<'HTML'
<main>
<section class="hero"><div class="wrap">
<h1>Documentation</h1>
<p>Installation, configuration and the reference pages. Written for the current release; older versions keep their own section.</p>
<a class="btn" href="#install">Start here</a>
</div></section>
<section id="install" class="wrap prose">
<h2>Installation</h2>
<p>Download the archive, unpack it into <code>/opt/halyard</code> and run the setup script. The script asks three questions and writes a configuration file next to the binary.</p>
<h2>Configuration</h2>
<p>Settings live in <code>/etc/halyard/config.toml</code>. Every option has a default, so a minimal file is often enough. Changes are applied with <code>halyard reload</code> — no restart, no dropped connections.</p>
</section>
<section id="reference" class="wrap prose">
<h2>Reference</h2>
<table>
<tr><th>Command</th><th>What it does</th></tr>
<tr><td><code>halyard status</code></td><td>Shows the running version and the state of each component</td></tr>
<tr><td><code>halyard check</code></td><td>Validates the configuration and the listening ports</td></tr>
<tr><td><code>halyard logs</code></td><td>Follows the log with readable timestamps</td></tr>
</table>
<p>Questions and patches are welcome through the issue tracker; the address is on the contact page.</p>
</section>
</main>
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 "<title>$t</title>" "$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 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://<IP-или-домен>/ ; состояние: kit local site --show, kit check"
}
kit_local_cmd() { kit_local_cmd() {
case ${1:-help} in case ${1:-help} in
help | -h | --help) kit_local_help ;; help | -h | --help) kit_local_help ;;
@@ -464,6 +870,7 @@ kit_local_cmd() {
passwd | pass) shift; kit_local_passwd "$@" ;; passwd | pass) shift; kit_local_passwd "$@" ;;
upgrades | auto) shift; kit_local_upgrades "$@" ;; upgrades | auto) shift; kit_local_upgrades "$@" ;;
hardening | hard) shift; kit_local_hardening "$@" ;; hardening | hard) shift; kit_local_hardening "$@" ;;
site | stub | stub-site) shift; kit_local_site "$@" ;;
*) *)
printf 'Неизвестная команда форка: %s\n' "$1" >&2 printf 'Неизвестная команда форка: %s\n' "$1" >&2
kit_local_help >&2 kit_local_help >&2