lightweight-server/agents/codex/done/2026-10-23-lightweight-security.md
ochenstarik-ui eb00e38a75
All checks were successful
Bash syntax / syntax (push) Successful in 27s
Bash syntax / syntax (pull_request) Successful in 26s
ci: pin checkout compatible with Forgejo runner
2026-10-23 12:35:27 +07:00

14 KiB
Raw Permalink Blame History

Lightweight-server: S1–S10, 23.10.2026

Технический результат: fixed, S8 — no_change (защита уже была в исходном main). Релизная приёмка и merge остаются отдельным gate; автор ветку не вливает. База: ef83559f7de19fd8649c78b22b05df397650cf46. Ветка: codex/lightweight-security-20261023.

Задание: agents/codex/inbox/2026-10-23-lightweight-security.md. Изолированный worktree существующего репозитория, без clone. На хосте и реальных RU/DE/FI/DK серверах установщики и root-команды не запускались. Создана собственная одноразовая KVM Ubuntu 24.04; после проверок остановлена, её диски, seed и временные SSH-ключи удалены.

Граница исправления

Установщики сами создавали публичные правила и wildcard-слушатели. Исправлена вся цепочка этапов 2/3/6, включая сохранённые управляемые правила и первый запуск upstream-установщика. Частичная правка одного списка портов оставляла бы прежние разрешения и другие этапы, открывающие их снова.

Пункт Изменение и подтверждение
S1 По умолчанию только текущий/будущий SSH и TCP 443. Нет автоматического 80/2096/40000/63636. Старые управляемые правила требуют явного сохранения; при отказе удаляются реальные UFW-правила IPv4/IPv6 и лишь затем обновляется файл состояния. Проверены реальные правила в VM.
S2 webListen=127.0.0.1, адрес проверяется через ss. Перед первым upstream start/restart временная обёртка systemctl применяет loopback к панели и отключает подписки; ошибка не допускает старт. Итоговая конфигурация проверяется до снятия защиты и после запуска.
S3 Режимы панели и подписок независимы. Подписки: loopback / выключены / явно публичные со случайным путём. Прежний валидный приватный путь сохраняется; достаточно длинный секретный публичный путь сохраняется по умолчанию. Ротация запрашивает подтверждение и предупреждает о поломке старых URL. Путь в журнал не выводится.
S4 Закреплены 3x-ui v3.10.2, Xray-install 05aa47266c94e6fb5ca48cad96afdb4becb5891f, Xray-core v26.10.21 и SHA-256 всех используемых исполняемых payload amd64/arm64. Локальный helper проверяет их перед запуском; неизвестные транзитивные загрузки 3x-ui блокируются. GitHub либо HTTPS Forgejo mirror, без fallback. В README загрузка самого проекта по полному проверенному SHA и доверенной сумме, без mutable main.
S5 После подтверждённого оператором нового входа удаляются правила 22/tcp и запись в управляемом списке. Если UFW всё ещё разрешает IPv4/IPv6 22 либо статус не проверен, скрипт не объявляет успех и сохраняет запись. Без подтверждения, при SSH=22 или MANAGE_UFW=no очистка не выполняется.
S6 Только PAM open_session. Отправка через фиксированный /usr/bin/setsid, отдельный процесс, stdin/stdout/stderr закрыты. Производственные пути конфигурации и журнала фиксированы, без тестовых environment overrides. Тест: close не отправляет, open запускает одну отправку, возврат <500ms при curl, задержанном на 1s.
S7 WARP — отдельный opt-in, по умолчанию выключен.
S8 Уже присутствовали --proto '=https' --tlsv1.2; исходный файл не изменён, регрессия закреплена новым тестом.
S9 Кавычки и обратные слэши экранируются в curl config, управляющие символы отклоняются до вызова curl. Проверены newline-инъекция и большой URL с vertical-tab, без pipefail/grep-q обхода. Секретная ссылка не передаётся в argv curl.
S10 Опциональный age encrypt streaming без временного plaintext-архива; SCP с BatchMode и строгим заранее проверенным known_hosts. Ошибка выгрузки — ошибка операции. Повтор initial проверяет checksum и повторяет выгрузку; несовместимый старый plaintext при выбранном age явно отклоняется без удаления снимка. В VM настоящий age/zstd/tar round-trip совпал с исходным fixture.

Файлы: этапы 2, user-3, tg-4, vpn-5, panel-warp-6, backup-7; новый ochenstarik-server-supply-chain.sh и справочный ...pins.txt; README EN/RU, CI workflow и четыре новых теста. Исходные восемь тестов не изменены.

Проверки по порядку

  1. find . -type f -name '*.sh' -print0 | xargs -0 -n1 bash -n — PASS.
  2. Новые поведенческие проверки:
    • tests/test-firewall-panel-security.sh: defaults, миграция старых UFW-правил, IPv6 residual error, sudo без SSH_CONNECTION, независимые режимы/сохранение путей, startup guard, ожидание слушателя, подтверждение удаления 22;
    • tests/test-download-security.sh: escaping/контрольные символы, cache shim lifecycle и запрет неизвестного installer URL, реальные вызовы helper из этапов 5/6, отсутствие upstream main, отсутствие fallback при ошибке mirror;
    • tests/test-notification-backup-security.sh: PAM open/close, detached delivery, строгие SSH options и ошибка offsite;
    • tests/test-backup-initial-retry.sh: реальный early-return путь initial, ошибка SCP → повтор, неизменность snapshot/checksum, age policy с прежним plaintext.
  3. for test in tests/*.sh; do bash "$test"; done — 12/12 PASS; git diff --check — PASS; diff исходных tests/ пуст.
  4. Одноразовая Ubuntu 24.04 amd64, настоящий systemd/UFW/3x-ui: этап 2 (полная установка), миграция старых правил, этап 6 fresh install и повтор с публичными подписками — exit 0.
  5. Тот же VM: helper prepare-xray → install-xray — exit 0, реальный Xray 26.10.21; age-backup round-trip — PASS.

Настоящие UFW и TCP-слушатели

Ubuntu image SHA-256: 04e8f2461a16a44e78a370284471f29616cdd9baf1893df25807b71f985b6c94, проверен по официальному SHA256SUMS cloud-images. SSH в тесте оставлен на 22; WARP выключен.

После полного этапа 2 и после удаления прежних 80/2096/40000/63636:

ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip
22/tcp          ALLOW IN Anywhere
443/tcp         ALLOW IN Anywhere
22/tcp (v6)     ALLOW IN Anywhere (v6)
443/tcp (v6)    ALLOW IN Anywhere (v6)

После fresh этапа 6 с приватной панелью и приватными подписками UFW остаётся тем же. Полный ss -ltn (сокращены только столбцы очередей):

Local Address:Port       Peer Address:Port
127.0.0.1:2096           0.0.0.0:*
127.0.0.1:2053           0.0.0.0:*
0.0.0.0:22              0.0.0.0:*
127.0.0.54:53           0.0.0.0:*
127.0.0.1:62789          0.0.0.0:*
127.0.0.53%lo:53        0.0.0.0:*
127.0.0.1:11111          0.0.0.0:*
[::]:22                 [::]:*

DNS/внутренние Xray-порты остаются локальными. Разрешение UFW 443 не означает, что скрипт создаёт на 443 отдельный HTTPS-сервис. Во время sampled fresh bootstrap наблюдались только 127.0.0.1:49587 (временный upstream порт), 127.0.0.1:2053, 127.0.0.1:2096; sampling дополняет pre-start enforcement, не доказывает отсутствие любого мгновенного слушателя сам по себе.

При явном публичном доступе к подпискам UFW дополнительно содержит:

2096/tcp         ALLOW IN Anywhere
2096/tcp (v6)    ALLOW IN Anywhere (v6)

ss -ltn: панель 127.0.0.1:2053, подписки *:2096. DB: webListen=127.0.0.1, subListen='', subEnable=true. Секретный путь не выгружался в журнал.

Два аудитора и собственная проверка

Дешёвые исполнители: lightweight_firewall_panel, lightweight_downloads_notifications_backup. До патча независимый boundary investigator; после кандидата два свежих read-only Codex auditor: lightweight_security_audit_one, lightweight_security_audit_two. Один цикл аудита, без самооценки авторского кода как релизного approval.

Подтверждены и исправлены найденные аудиторами: сохранённые UFW entries, игнорирование ошибки удаления 22, IPv6 статус, потеря/ротация прежнего subscription path, initial/offsite retry и несовместимая age policy, неточное описание pin manifest. Root дополнительно воспроизвёл реальные ошибки: sudo очищает SSH_CONNECTION, повторный mkdir shim и readiness race после systemctl start. После исправления повторил focused, все repository checks и настоящий VM-прогон. Неподтверждённое замечание об обязательной root escalation через PAM аудитор отозвал; тестовые env overrides всё равно удалены из производства.

Evidence и пределы

Сырые обезличенные результаты сохранены в managed Codex Security artifact storage, target lightweight-server, bundle artifacts-827d7430ed6521a319c17103be0c53cc1d742842b3e097b3a5f63693cead15d9, каталог artifacts/lightweight-20261023/:

Файл SHA-256
final-checks.log 57be6a409fbce5488611bbc853eaecaacecdde8fa7c40656b87fb2ec925970f2
ubuntu24-step2.log 6e37718bf82ef1bf061449dee8782d79855946dca2ba5776e6bc9bada3dbec37
ubuntu24-migration.log 0257a0c97a8dd592c3492d886ad6fb42af2fedff7139eb70ea16387a8b2e2bab
ubuntu24-private.log 4de0099004bcb79822b5a915d1e5914ea3b447e8b41e67471c730f75f39553c8
ubuntu24-public.log f0d75dd045e89ca9f8066542e4806b20c10724165e07f56f659b35c83728274f
ubuntu24-age.log 737740b39ceba68214737f21283fea1d37a5f448c5bb3addcb6eef1d39fbcc09
ubuntu24-xray.log 270af1adae421035c3d97d4b0779c2d2f922e72208988036a3e534cff27c2a14

Полная VPN-маршрутизация реального сервера, WARP opt-in, arm64 execution и реальный SCP на чужой хост не запускались; вне требуемого Ubuntu amd64 прогона и разрешений. Mirror проверен на fail-closed отказ; готовый публичный mirror релиз не опубликован, нужны файлы с указанными суммами. Блок README требует реально одобренные SHA+checksum и не выдумывает готовый release.

По последнему правилу владельца новый PR ждёт Grok на точном SHA либо Claude по прямому запросу владельца. Два технических Codex-аудита этот release gate не заменяют. CI удалённой ветки проверяется отдельно после публикации; этот отчёт фиксирует локальные/VM результаты.

Поправка CI после публикации

Первоначальный SHA 9f06756cefb56be41fb815edcf526f2eef924ef4, автоматические push/PR runs 1/2 (tasks 7364/7365): FAIL до checkout, не отказ тестов. Forgejo 9.0.3, runner v6.4.0 отклонил runs.using=node24 существующего checkout@v6. Workflow закреплён на совместимом Node20 checkout v4.2.2, точный upstream/mirror SHA 11bd71901bbe5b1630ceea73d27597364c9af683; action.yml проверен из Forgejo mirror. Все 12 проверок сохранены. Результат автоматического CI нового head фиксируется в PR и AI-Memory; повторный dispatch не выполнялся.