Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,32 @@ on:
branches: [ main ]
pull_request:

# Токен раннера — только на чтение репозитория (ни один шаг не пишет в GitHub).
permissions:
contents: read

jobs:
# ── Статический анализ всех скриптов ────────────────────────────────────────
shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# SHA-пин: плавающий тег v4 может быть перенесён на другой коммит
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install shellcheck
run: sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck
- name: Gate (severity=error — блокирует merge)
run: shellcheck -x --severity=error install.sh scripts/*.sh scripts/lib/*.sh
run: shellcheck -x --severity=error install.sh scripts/*.sh scripts/lib/*.sh tests/*.sh
- name: Report (severity=style — информационно, не блокирует)
continue-on-error: true
run: shellcheck -x --severity=style install.sh scripts/*.sh scripts/lib/*.sh
- name: apply-path smoke (set -u — полный apply protect.sh через стабы)
# Ловит unbound-vars в ветках, исполняемых только при реальном применении
# (модули fleet/blocklists/ctguard, маркер, save_conf) — их не видит DRY_RUN/bash -n.
run: bash tests/apply-smoke.sh
- name: fleet-sync unit (ИСПОЛНЯЕТ сгенерированный хелпер против стабов)
# Секреты не в argv, уходят через -H @file, URL в логе без userinfo,
# редиректы при токене заблокированы, fail-safe last-known-good работает.
run: bash tests/fleet-sync-unit.sh

# ── Smoke-тесты на матрице дистрибутивов ────────────────────────────────────
# Ловит ровно тот класс регрессий, из-за которых появился этот PR: установка
Expand All @@ -46,7 +55,8 @@ jobs:
steps:
- name: Install git (нужен checkout) + базовые сертификаты
run: apt-get update -qq && apt-get install -y -qq --no-install-recommends git ca-certificates
- uses: actions/checkout@v4
# SHA-пин: плавающий тег v4 может быть перенесён на другой коммит
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Prep container + runtime-зависимости
run: |
# Запрещаем запуск сервисов из postinst (в контейнере нет systemd как PID1)
Expand Down Expand Up @@ -95,6 +105,19 @@ jobs:
if NODE_PORT=abc DRY_RUN=1 REMNAWAVE_NONINTERACTIVE=1 bash scripts/protect.sh 2>/dev/null; then
echo "валидация НЕ сработала на кривом NODE_PORT"; exit 1
else echo "ok: кривой NODE_PORT отвергнут"; fi
- name: protect.sh DRY_RUN (порт текущей SSH-сессии не теряется → nft -c)
run: |
# SSH_CONNECTION = "<c-ip> <c-port> <s-ip> <s-port>": порт сессии — ground truth.
# Ошибка детекта/протухший protect.conf не должны отрезать текущий доступ.
OUT="$(SSH_CONNECTION='203.0.113.9 55000 10.0.0.5 56777' SSH_PORT=22 \
DRY_RUN=1 REMNAWAVE_NONINTERACTIVE=1 bash scripts/protect.sh)"
echo "$OUT" | grep -q 'SSH-сессия пришла на :56777' || { echo "порт SSH-сессии не подхвачен"; exit 1; }
# в DRY_RUN ruleset генерится во временный файл (/tmp/na_filter.XXXXXX.nft),
# а не в $CONF_DIR — берём самый свежий
F="$(ls -t /tmp/na_filter.*.nft 2>/dev/null | head -1)"
[ -n "$F" ] || { echo "не найден сгенерированный ruleset"; exit 1; }
grep -q 'tcp dport { 22, 56777 }' "$F" || { echo "в ruleset ($F) нет обоих SSH-портов"; exit 1; }
echo "ok: порт сессии добавлен в ruleset ($F)"
- name: diagnose.sh (read-only, не должен падать жёстко)
run: bash scripts/diagnose.sh || true
- name: diagnose.sh --json (machine-readable smoke)
Expand Down
107 changes: 107 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,112 @@
# Changelog

## v3.9.2 — 2026-07-26

Релиз по итогам сплошного аудита кодовой базы (бэкдоры/баги/supply-chain). Бэкдоров и
утечек данных не нашли; всё ниже — реальные баги и ужесточения. Дефолты поведения не
меняются, кроме перечисленного в «Совместимости».

### 🛡 Анти-локаут: сейфти-таймер больше не оставляет мину на ребут
- **Сработавший сейфти снимал ТОЛЬКО живую таблицу**, а `na-firewall.service` оставался
в автозагрузке. Сценарий: прогон отрезал SSH → через `SAFETY_DELAY` доступ вернулся →
оператор увидел живой бокс и ушёл → **первый же ребут применил тот же локаут-руллсет
заново, уже без сейфти**. Теперь откат выполняет `/usr/local/sbin/na-fw-safety-revert`:
снимает таблицу **и** выключает `na-firewall.service`, пишет метку `safety-fired.last`
и громкую строку в journald.
- `na-diagnose` получил два сенсора: **«сейфти срабатывал»** (защиты сейчас нет — нужен
повторный прогон) и **«таблица есть, а автозагрузка выключена»** (правила не переживут
ребут). В `--json` — поля `safety_fired_age_s` и `fw_boot_enabled`.

### 🛡 Анти-локаут: SSH-порт определяется правильно и не теряется
- **`detect_ssh_port` был слеп к socket-активации**: на Ubuntu 24.04+ слушает `ssh.socket`
(владелец сокета — `systemd`, `Port` из `sshd_config` игнорируется), старая ss-эвристика
молча возвращала `22`, и strict открывал не тот порт. Хуже всего, что штатная проверка
«зайди новым окном» этого не ловит: IP админа целиком в whitelist — у него работает, а
у всех остальных локаут после снятия сейфти. Новый порядок источников: активный
`ssh.socket`/`sshd.socket` → **`sshd -T`** (раскрывает `Include sshd_config.d/*.conf`,
куда порт кладёт cloud-init) → `ss` → `sshd_config` → `22`.
- **Порт текущей SSH-сессии теперь всегда попадает в правила.** `SSH_CONNECTION` даёт
ground truth (сессию уже принял sshd), поэтому ошибка детекта, протухший `protect.conf`
или смена порта между прогонами больше не могут отрезать тот самый доступ, из которого
идёт установка: правила ставятся на **оба** порта + громкий warn (та же логика, что для
node-порта в v3.8). `SSH_PORT` принимает список; в маркер пишется эффективный список,
в `protect.conf` — intent оператора.

### ⚡ XanMod снова ставится на «грязных» нодах
- `setup_xanmod_repo` проверял репозиторий **неskoupленным `apt-get update`**, а тот
возвращает rc≠0 из-за ЛЮБОГО чужого битого источника на боксе (типовой съёмный VPS).
Живой XanMod-репо объявлялся мёртвым, фоллбэк на `bookworm` падал так же — ядро молча
не ставилось с диагнозом «репо недоступен». Теперь вердикт выносится по своему list
(`Dir::Etc::sourcelist`) — как уже было сделано для CrowdSec в v3.6.
- Для **bookworm** XanMod оставил в репо только LTS-ветку: запрос `main`/`edge`/`rt`
автоматически переводится в `lts` с info-строкой вместо «пакет не найден».

### 🛡 CrowdSec: канонический `any/any` (и Debian 13)
- Suite-кандидаты теперь `any/any` → `<os>/<codename>` → `bookworm`/`noble`. На **Debian 13
(trixie)** у CrowdSec **нет своего suite** (в `debian/trixie` отсутствует `Release`), а
пакет из самого дистрибутива — устаревший 1.4.6; `any/any` это официальный путь апстрима.
- **`CROWDSEC_STRICT=1`** — запретить `curl|bash`-фоллбэк на `install.crowdsec.net`.
Фоллбэк форсируется атакующим: достаточно сделать packagecloud недостижимым
(egress-фильтр, DNS), чтобы проверенный по отпечатку APT-репо подменился на
неверифицированный код из сети, запускаемый root'ом.

### 🔐 Ключи, токены, логи
- **Пиннинг ключей стал настоящим.** Проверка «наш отпечаток есть среди импортированных»
пропускала лишние ключи, приехавшие тем же блобом, — а apt по `signed-by=` доверяет
**каждому** ключу keyring'а. Особенно важно для фоллбэка XanMod: он ищет ключ на
keyserver **по 64-битному keyid**, коллизию которого сделать дёшево. Новый
`import_pinned_key` импортирует во временный GNUPGHOME и экспортирует в keyring
**ровно пиненый отпечаток**, затем проверяет, что первичный ключ там ровно один и наш.
- **Секреты ушли из argv.** `na-fleet-sync` передавал `X-Api-Key`/`Authorization` через
`-H` в командной строке — аргументы процесса видны всей системе в `/proc/<pid>/cmdline`
на всё время запроса. Теперь заголовки пишутся в файл внутри `mktemp -d` (0700) и
передаются как `-H @файл`.
- **Пароль больше не течёт в journald.** README сам предлагает закрывать
`REMNAWAVE_NODES_URL` basic-auth'ом, а хелпер логировал URL целиком — то есть
`user:pass@` попадал в системный журнал каждые 5 минут (и повторялся в `na-fw-status`).
Теперь userinfo зачищается (`***@host`).
- Задан `http://`-URL при наличии токена — громкий warn (редирект-даунгрейд мы блокируем
`--proto-redir`, но явную cleartext-схему оператор указывает сам).
- `install.sh` качает модули и подписи с `--proto '=https' --proto-redir '=https'` —
редирект на cleartext http больше невозможен.

### 🐛 Прочие починки
- **`swapon` больше не роняет весь optimize.** На btrfs/CoW (`swapfile has holes`) или при
запрете свапа у хостера падение под `set -e` обрывало прогон **посередине**: sysctl и
лимиты применены, а journald-cap, THP, governor, маркер и `save_conf` — уже нет, и
тулкит про это не знал. Теперь ошибка → warn и продолжение. dd-фоллбэк уважает
`REMNAWAVE_SWAP_SIZE` (был хардкод 2048 МБ).
- **`FLEET_SYNC_INTERVAL` персистится**, а `na-diagnose` читает его из таймера (или из
`protect.conf` правильным парсером). Старый парс `^FLEET_SYNC_INTERVAL=` не совпадал
**никогда** — конфиг пишется идиомой `: "${KEY:=value}"`, поэтому интервал всегда
считался равным 5 мин и любой более редкий синк выглядел «протухшим токеном».
- `na-diagnose`: строка про решения CrowdSec больше не печатает двойной `0`
(`grep -vc` отдаёт `0` и rc=1 одновременно → `|| echo 0` дописывал второй).
- **`flock` на прогон** `protect.sh`/`optimize.sh`: параллельные запуски (оркестратор из
панели + руками) перекрывались — сейфти-таймер первого удалял таблицу, которую
применил и подтвердил второй. Отключается `NA_NO_LOCK=1`.
- **`rollback` доубирает за собой**: строки `pam_limits` из `common-session*` и `/swapfile`
вместе с записью в `/etc/fstab` — но swap снимается **только** если его создали мы
(метка `swapfile.created`) и только если он не занят.
- `install.sh -h/--help` работает в curl|bash-режиме (там `$0` = `bash`, и `sed` по нему
просто падал) и больше не требует root.

### 🧪 Тесты и CI
- **`tests/fleet-sync-unit.sh`** — новый тест, который **исполняет** сгенерированный
`na-fleet-sync` против стабов curl/nft/logger: секреты не в argv, реально уходят через
`-H @file`, URL в логе без userinfo, при токене нет `-L`/есть `--max-redirs 0`, парсинг
адресов (в т.ч. CRLF и IPv6) и fail-safe last-known-good. Раньше фикс v3.9.1 был покрыт
только `grep`-ом по тексту хелпера.
- `apply-smoke` проверяет аварийный откат сейфти и подхват порта SSH-сессии; CI гоняет
shellcheck и по `tests/*.sh`, а в матрице появился прогон с `SSH_CONNECTION`.
- CI: `actions/checkout` пиннится по SHA, добавлен `permissions: contents: read`.

### ⚠️ Совместимость
- Правила SSH рендерятся как `tcp dport { … }` (набор портов) вместо одиночного порта —
ruleset функционально прежний.
- `crowdsec.list` при ре-ране переписывается на `any/any` (пакеты те же, ключ тот же).
- Ре-ран на ноде, где сейфти когда-то срабатывал, чистит метку `safety-fired.last`.

## v3.9.1 — 2026-07-21

### 🛡 fleet-sync — `X-Api-Key` больше не утекает на редиректе
Expand Down
Loading
Loading