Переезд: зоопарк дашбордов в LXC на новом хосте, сертификаты 13 сайтов перевыпустить, 94.228.121.73 погасить #3031

Open
opened 2026-08-21 12:55:52 +00:00 by lekss361 · 6 comments
Owner

Эпик: #2989

Что переезжает

Сервер дашбордов 94.228.121.73: 51 systemd-юнит, PHP-FPM, MariaDB, Postgres geowatch, WordPress, 13 публичных сайтов. Всё это селится на новый выделенный сервер и занимает ~4 ГБ.

Только LXC, не KVM

Контейнер делит ядро и page cache с хостом. Виртуалка нарежет память намертво: у KVM выделение жёсткое, каждый гость заводит свой page cache, полная нарезка на Proxmox с ZFS съедает 15–17 ГБ. ~90 % выгоды изоляции берётся без гипервизора — cgroup v2, отдельные docker-сети, отдельный раздел под storage.

Сертификаты не мигрировать

Caddy на хосте фронтит LXC по Host-заголовку и выпускает сертификаты заново — это надёжнее переноса ключей. Перенос приватных ключей ACME между машинами регулярно ломается на правах и на состоянии аккаунта.

Зависимость: перевыпуск возможен только после того, как DNS указывает на новый адрес → связано с задачей про TTL.

Принятый риск

WordPress и 13 публичных сайтов приезжают на хост с базой ПДн. LXC без сетевого доступа к подсети Postgres и без доступа к docker-сокету снижает риск, но не убирает: WordPress — типовая точка входа. Решение принято владельцем осознанно. В рамках задачи обязательно: LXC не видит подсеть Postgres, не имеет docker-сокета, не ходит в S3-креды.

Гашение старого сервера

После подтверждения работы всех 13 сайтов — погасить 94.228.121.73. Цена этого сервера неизвестна, экономия от гашения не посчитана — выяснить и записать, это влияет на итоговую экономику переезда.

Приёмка

  • Зоопарк поднят в LXC, 51 юнит запущен
  • 13 сайтов отвечают 200 по HTTPS с новыми сертификатами
  • LXC изолирован: нет доступа к подсети Postgres, нет docker-сокета
  • Цена старого сервера выяснена и зафиксирована
  • 94.228.121.73 погашен
Эпик: #2989 ## Что переезжает Сервер дашбордов `94.228.121.73`: **51 systemd-юнит**, PHP-FPM, MariaDB, Postgres geowatch, WordPress, 13 публичных сайтов. Всё это селится на новый выделенный сервер и занимает ~4 ГБ. ## Только LXC, не KVM Контейнер делит ядро и page cache с хостом. Виртуалка нарежет память намертво: у KVM выделение жёсткое, каждый гость заводит свой page cache, полная нарезка на Proxmox с ZFS съедает 15–17 ГБ. ~90 % выгоды изоляции берётся без гипервизора — cgroup v2, отдельные docker-сети, отдельный раздел под storage. ## Сертификаты не мигрировать Caddy на хосте фронтит LXC по Host-заголовку и **выпускает сертификаты заново** — это надёжнее переноса ключей. Перенос приватных ключей ACME между машинами регулярно ломается на правах и на состоянии аккаунта. Зависимость: перевыпуск возможен только после того, как DNS указывает на новый адрес → связано с задачей про TTL. ## Принятый риск **WordPress и 13 публичных сайтов приезжают на хост с базой ПДн.** LXC без сетевого доступа к подсети Postgres и без доступа к docker-сокету снижает риск, но не убирает: WordPress — типовая точка входа. Решение принято владельцем осознанно. В рамках задачи обязательно: LXC не видит подсеть Postgres, не имеет docker-сокета, не ходит в S3-креды. ## Гашение старого сервера После подтверждения работы всех 13 сайтов — погасить `94.228.121.73`. **Цена этого сервера неизвестна**, экономия от гашения не посчитана — выяснить и записать, это влияет на итоговую экономику переезда. ## Приёмка - [ ] Зоопарк поднят в LXC, 51 юнит запущен - [ ] 13 сайтов отвечают 200 по HTTPS с новыми сертификатами - [ ] LXC изолирован: нет доступа к подсети Postgres, нет docker-сокета - [ ] Цена старого сервера выяснена и зафиксирована - [ ] `94.228.121.73` погашен
lekss361 added the
chore
scope/devops
tradein
labels 2026-08-21 12:57:10 +00:00
Author
Owner

Сетевое разделение на новом хосте — решено 2026-08-22

В задаче выбран LXC и описано, что Caddy фронтит его по Host-заголовку, но про сети не сказано ничего. После переезда это становится главным риском хоста, поэтому фиксирую раскладку.

Что оказывается на одной машине

Тринадцать публичных PHP-сайтов на WordPress с MariaDB — и боевая база Меры. Поверхность атаки WordPress несопоставима с нашим Next.js + FastAPI. Компрометация одного сайта не должна давать ни одного пути к tradein-postgres.

Сегодня на Beget этой проблемы нет: дашборды живут на отдельной машине 94.228.121.73. Переезд её создаёт.

Раскладка

Уровень Что Досягаемость
Хост Caddy видит всех; из интернета видны только 80/443
Docker, сеть приложений backend, frontend, Redis, скраперы, браузер между собой по именам
Docker, сеть данных Postgres только из сети приложений
LXC, свой мост 13 сайтов, PHP-FPM, MariaDB никакой связи с сетями Docker

Требования

  • LXC на отдельном мосту, не подключён ни к tradein-net, ни к gendesign_shared
  • Учётных данных Меры внутри LXC нет вообще — ни в env, ни в конфигах, ни в бэкапах
  • Исходящие соединения из LXC ограничены (WordPress не должен свободно ходить наружу)
  • Трафик к LXC идёт только снаружи внутрь, через Caddy по Host-заголовку
  • Postgres вынесен в отдельную docker-сеть; из сети приложений доступен, из прочего — нет
  • Проверка после настройки: из LXC нет TCP-доступа к порту Postgres (тест, а не предположение)

Отдельно: пересмотреть gendesign_shared

Сейчас это внешняя сеть, в которую заведены Caddy, Redis и tradein-postgres — решение принималось для одной машины без соседей (#2709: Redis введён в gendesign_shared, чтобы tradein-backend вообще мог до него достучаться).

На машине с тринадцатью WordPress-сайтами такую общую сеть нельзя переносить как есть. Разделение на «сеть приложений» и «сеть данных» — минимум.

Чего в раскладке намеренно нет

Виртуалок. Обоснование в теле задачи остаётся: KVM оправдан при потребности в другом ядре, живой миграции или жёсткой границе против недоверенного кода — ничего из этого нет, а память гипервизор вырезает намертво.

Дополнительной изоляции наших сервисов. Меру и Site Finder заворачивать поверх Docker в LXC незачем: сломается конвейер деплоя, а границы даёт описание сетей, а не гипервизор.

Контейнеризации легаси-сайтов. Недели работы без отдачи; LXC для того и выбран, чтобы они продолжали вести себя как на старом сервере.

Про репозитории

Дашборды — в отдельный репозиторий: другой стек, другой жизненный цикл, наш CI их не трогает.

Монорепо Gendesign + Мера сейчас не делить. Они делят CI, правила и Caddyfile, а конвейер и так выворачивается наизнанку в #3029. Две перестройки одновременно не дадут понять, что именно сломалось. Вернуться после переезда.

## Сетевое разделение на новом хосте — решено 2026-08-22 В задаче выбран LXC и описано, что Caddy фронтит его по Host-заголовку, но **про сети не сказано ничего**. После переезда это становится главным риском хоста, поэтому фиксирую раскладку. ### Что оказывается на одной машине Тринадцать публичных PHP-сайтов на WordPress с MariaDB — и боевая база Меры. Поверхность атаки WordPress несопоставима с нашим Next.js + FastAPI. Компрометация одного сайта не должна давать ни одного пути к `tradein-postgres`. Сегодня на Beget этой проблемы нет: дашборды живут на отдельной машине `94.228.121.73`. Переезд её создаёт. ### Раскладка | Уровень | Что | Досягаемость | |---|---|---| | Хост | Caddy | видит всех; из интернета видны только 80/443 | | Docker, сеть приложений | backend, frontend, Redis, скраперы, браузер | между собой по именам | | Docker, сеть данных | Postgres | **только из сети приложений** | | LXC, свой мост | 13 сайтов, PHP-FPM, MariaDB | **никакой связи с сетями Docker** | ### Требования - [ ] LXC на отдельном мосту, **не подключён** ни к `tradein-net`, ни к `gendesign_shared` - [ ] Учётных данных Меры внутри LXC нет вообще — ни в env, ни в конфигах, ни в бэкапах - [ ] Исходящие соединения из LXC ограничены (WordPress не должен свободно ходить наружу) - [ ] Трафик к LXC идёт только снаружи внутрь, через Caddy по Host-заголовку - [ ] Postgres вынесен в отдельную docker-сеть; из сети приложений доступен, из прочего — нет - [ ] Проверка после настройки: из LXC нет TCP-доступа к порту Postgres (тест, а не предположение) ### Отдельно: пересмотреть `gendesign_shared` Сейчас это внешняя сеть, в которую заведены Caddy, Redis и `tradein-postgres` — решение принималось для одной машины без соседей (#2709: Redis введён в `gendesign_shared`, чтобы `tradein-backend` вообще мог до него достучаться). На машине с тринадцатью WordPress-сайтами такую общую сеть нельзя переносить как есть. Разделение на «сеть приложений» и «сеть данных» — минимум. ### Чего в раскладке намеренно нет **Виртуалок.** Обоснование в теле задачи остаётся: KVM оправдан при потребности в другом ядре, живой миграции или жёсткой границе против недоверенного кода — ничего из этого нет, а память гипервизор вырезает намертво. **Дополнительной изоляции наших сервисов.** Меру и Site Finder заворачивать поверх Docker в LXC незачем: сломается конвейер деплоя, а границы даёт описание сетей, а не гипервизор. **Контейнеризации легаси-сайтов.** Недели работы без отдачи; LXC для того и выбран, чтобы они продолжали вести себя как на старом сервере. ### Про репозитории Дашборды — в отдельный репозиторий: другой стек, другой жизненный цикл, наш CI их не трогает. Монорепо Gendesign + Мера **сейчас не делить**. Они делят CI, правила и Caddyfile, а конвейер и так выворачивается наизнанку в #3029. Две перестройки одновременно не дадут понять, что именно сломалось. Вернуться после переезда.
Author
Owner

Заходил снять инвентарь зоопарка (это предпосылка для всего остального в задаче — без списка юнитов и сайтов нечего переносить и нечем проверять приёмку). Доступ есть, но снять успел мало: сервер меня отрезал, и дальше я стучаться не стал.

Что успел

hostname: 5880115-ai735999
uptime:   303 дня, load average 0.09 0.06 0.01

Ключ prinzip_marketing рабочий, root@94.228.121.73 пускает.

Что произошло дальше

Первая команда прошла. Вторая (многострочная, собирала ОС/железо/диск/список юнитов) — Connection timed out. Третья — Connection reset by peer. Четвёртая, уже одиночная и короткая, — снова timed out.

Похоже на fail2ban или аналогичный IDS, среагировавший на оборванное соединение. Дальше долбиться в чужой продакшн в час ночи не стал — это сервер Антона, живой, с 13 публичными сайтами, и настойчивые переподключения там неуместны, тем более что бан, скорее всего, сам истечёт.

Что нужно, чтобы снять инвентарь с одного захода

Когда бан отпустит, всё собирается одной сессией. Команды подобраны так, чтобы не читать ничего с секретами (wp-config.php, .env, дампы БД не трогаются):

ssh -i ~/.ssh/prinzip_marketing root@94.228.121.73 '
  . /etc/os-release; echo "ОС: $PRETTY_NAME  ядро: $(uname -r)  CPU: $(nproc)"
  free -h  | awk "/Mem/{print \"RAM: \"\$2\" / занято \"\$3}"
  df -h /  | tail -1
  echo "--- enabled:"; systemctl list-unit-files --state=enabled --type=service --no-legend | wc -l
  echo "--- running:"; systemctl list-units --type=service --state=running --no-legend | awk "{print \$1}"
  echo "--- слушающие порты:"; ss -lntp
  echo "--- вхосты nginx/apache:"; ls /etc/nginx/sites-enabled/ /etc/apache2/sites-enabled/ 2>/dev/null
  echo "--- базы:"; mysql -e "SHOW DATABASES" 2>/dev/null; sudo -u postgres psql -lqt 2>/dev/null | cut -d\| -f1
  echo "--- размер данных:"; du -sh /var/www /var/lib/mysql /var/lib/postgresql 2>/dev/null
'

Разумнее выполнить это одной сессией и не спеша, а не серией коротких подключений — именно серия, судя по всему, и вызвала бан.

Замечание по приёмке

Из пяти пунктов приёмки без владельца закрываемы только первые три, и то не полностью:

Пункт Кто может
Зоопарк поднят в LXC, 51 юнит запущен я — после инвентаря
13 сайтов отвечают 200 по HTTPS с новыми сертификатами требует DNS на новый адрес → упирается в #3027 (TTL, владелец)
LXC изолирован (нет подсети Postgres, нет docker-сокета) я — проверяемо и настраиваемо на Poincare независимо
Цена старого сервера выяснена только владелец (панель хостера Антона)
94.228.121.73 погашен только владелец

Мост zoobr0 на Poincare уже поднят и в ufw описан (67/udp, 53/udp, 53/tcp на нём, плюс DENY FWD 172.16.0.0/12 from 10.100.0.0/24 — то есть форвард из зоо-подсети в докерные сети уже запрещён). То есть третий пункт приёмки частично уже сделан прошлым bootstrap'ом; проверю его отдельно и распишу, чего не хватает.

Заходил снять инвентарь зоопарка (это предпосылка для всего остального в задаче — без списка юнитов и сайтов нечего переносить и нечем проверять приёмку). Доступ есть, но снять успел мало: **сервер меня отрезал**, и дальше я стучаться не стал. ## Что успел ``` hostname: 5880115-ai735999 uptime: 303 дня, load average 0.09 0.06 0.01 ``` Ключ `prinzip_marketing` рабочий, `root@94.228.121.73` пускает. ## Что произошло дальше Первая команда прошла. Вторая (многострочная, собирала ОС/железо/диск/список юнитов) — `Connection timed out`. Третья — `Connection reset by peer`. Четвёртая, уже одиночная и короткая, — снова `timed out`. Похоже на fail2ban или аналогичный IDS, среагировавший на оборванное соединение. **Дальше долбиться в чужой продакшн в час ночи не стал** — это сервер Антона, живой, с 13 публичными сайтами, и настойчивые переподключения там неуместны, тем более что бан, скорее всего, сам истечёт. ## Что нужно, чтобы снять инвентарь с одного захода Когда бан отпустит, всё собирается одной сессией. Команды подобраны так, чтобы не читать ничего с секретами (`wp-config.php`, `.env`, дампы БД не трогаются): ```bash ssh -i ~/.ssh/prinzip_marketing root@94.228.121.73 ' . /etc/os-release; echo "ОС: $PRETTY_NAME ядро: $(uname -r) CPU: $(nproc)" free -h | awk "/Mem/{print \"RAM: \"\$2\" / занято \"\$3}" df -h / | tail -1 echo "--- enabled:"; systemctl list-unit-files --state=enabled --type=service --no-legend | wc -l echo "--- running:"; systemctl list-units --type=service --state=running --no-legend | awk "{print \$1}" echo "--- слушающие порты:"; ss -lntp echo "--- вхосты nginx/apache:"; ls /etc/nginx/sites-enabled/ /etc/apache2/sites-enabled/ 2>/dev/null echo "--- базы:"; mysql -e "SHOW DATABASES" 2>/dev/null; sudo -u postgres psql -lqt 2>/dev/null | cut -d\| -f1 echo "--- размер данных:"; du -sh /var/www /var/lib/mysql /var/lib/postgresql 2>/dev/null ' ``` Разумнее выполнить это **одной сессией и не спеша**, а не серией коротких подключений — именно серия, судя по всему, и вызвала бан. ## Замечание по приёмке Из пяти пунктов приёмки без владельца закрываемы только первые три, и то не полностью: | Пункт | Кто может | |---|---| | Зоопарк поднят в LXC, 51 юнит запущен | я — **после** инвентаря | | 13 сайтов отвечают 200 по HTTPS с новыми сертификатами | требует DNS на новый адрес → упирается в #3027 (TTL, владелец) | | LXC изолирован (нет подсети Postgres, нет docker-сокета) | я — проверяемо и настраиваемо на Poincare независимо | | Цена старого сервера выяснена | **только владелец** (панель хостера Антона) | | `94.228.121.73` погашен | **только владелец** | Мост `zoobr0` на Poincare уже поднят и в `ufw` описан (`67/udp`, `53/udp`, `53/tcp` на нём, плюс `DENY FWD 172.16.0.0/12 from 10.100.0.0/24` — то есть форвард из зоо-подсети в докерные сети уже запрещён). То есть третий пункт приёмки частично уже сделан прошлым bootstrap'ом; проверю его отдельно и распишу, чего не хватает.
Author
Owner

Третий пункт приёмки — изоляция LXC — проверен эмпирически и держится. Проверял не чтением правил, а попытками пробиться изнутри контейнера.

Что уже стоит на Poincare

zoobr0:  10.100.0.1/24, UP
lxc list:  zoo | RUNNING | 10.100.0.240 (eth0) | CONTAINER

Порядок правил важнее их наличия, поэтому смотрел сырой iptables, а не вывод ufw status:

Chain ufw-user-forward
1  DROP    10.100.0.0/24 -> 172.16.0.0/12      <- ПЕРВЫМ
2  ACCEPT  10.100.0.0/24 -> 0.0.0.0/0
3  ACCEPT  0.0.0.0/0     -> 10.100.0.0/24

DROP стоит правилом №1, раньше обоих ACCEPT. 172.16.0.0/12 накрывает все три docker-подсети: bridge 172.17.0.0/16, gendesign_shared 172.18.0.0/16, tradein-net 172.19.0.0/16.

Проверка изнутри zoo

Цель Ожидание Результат
1.1.1.1:443 (наружу) доступен ДОСТУПЕН
tradein-postgres 172.18.0.2:5432 недоступен таймаут (пакет отброшен)
шлюз gendesign_shared 172.18.0.1:5432 недоступен таймаут
docker default bridge 172.17.0.1:22 недоступен таймаут
/var/run/docker.sock отсутствует No such file or directory
S3-креды (env по AWS/S3_/SELECTEL, /root/.aws) нет 0 совпадений, каталога нет
lxc config show zoo → devices пусто devices: {} — ни одного проброса

DNS изнутри работает (deb.debian.org резолвится), исходящий трафик ходит — то есть изоляция режет именно доступ внутрь хоста, а не всё подряд.

Про методику — почему первый заход не считается

Сначала пробовал </dev/tcp/HOST/PORT через lxc exec ... bash -c. Все цели вернули 124, включая контрольную — а контроль обязан был пройти. Это значит, что не изоляция сработала, а проба не работала вовсе (/dev/tcp в этом bash недоступен), и любой вывод «изоляция держит» из тех цифр был бы выдуман.

Переделал на python3 socket.create_connection с двумя контролями. Положительный контроль (1.1.1.1:443) прошёл — значит механика замера рабочая, и таймауты по docker-подсетям означают именно DROP.

Отдельно: 10.100.0.1:22 (sshd хоста) из контейнера тоже недоступен — и это намеренно, в ufw явным правилом 22/tcp DENY from 10.100.0.0/24. То есть зоопарк не сможет ходить по ssh на хост даже при компрометации WordPress. Это сверх того, что требует приёмка.

Статус пункта

  • LXC изолирован: нет доступа к подсети Postgres, нет docker-сокета — подтверждено пробами, не чтением конфигов.

Оговорка: проверен контейнер zoo в текущем виде — пустой, без зоопарка. Когда в него приедут 51 юнит и WordPress, проверку надо повторить той же пробой: новые сервисы могут потребовать пробросов, и любой проброс (devices) способен эту изоляцию открыть. Команды выше воспроизводимы как есть.

Третий пункт приёмки — **изоляция LXC — проверен эмпирически и держится.** Проверял не чтением правил, а попытками пробиться изнутри контейнера. ## Что уже стоит на Poincare ``` zoobr0: 10.100.0.1/24, UP lxc list: zoo | RUNNING | 10.100.0.240 (eth0) | CONTAINER ``` Порядок правил важнее их наличия, поэтому смотрел сырой iptables, а не вывод `ufw status`: ``` Chain ufw-user-forward 1 DROP 10.100.0.0/24 -> 172.16.0.0/12 <- ПЕРВЫМ 2 ACCEPT 10.100.0.0/24 -> 0.0.0.0/0 3 ACCEPT 0.0.0.0/0 -> 10.100.0.0/24 ``` DROP стоит правилом №1, раньше обоих ACCEPT. `172.16.0.0/12` накрывает все три docker-подсети: `bridge` 172.17.0.0/16, `gendesign_shared` 172.18.0.0/16, `tradein-net` 172.19.0.0/16. ## Проверка изнутри `zoo` | Цель | Ожидание | Результат | |---|---|---| | `1.1.1.1:443` (наружу) | доступен | **ДОСТУПЕН** | | `tradein-postgres` 172.18.0.2:5432 | недоступен | таймаут (пакет отброшен) | | шлюз `gendesign_shared` 172.18.0.1:5432 | недоступен | таймаут | | docker default bridge 172.17.0.1:22 | недоступен | таймаут | | `/var/run/docker.sock` | отсутствует | `No such file or directory` | | S3-креды (`env` по AWS/S3_/SELECTEL, `/root/.aws`) | нет | 0 совпадений, каталога нет | | `lxc config show zoo` → devices | пусто | `devices: {}` — ни одного проброса | DNS изнутри работает (`deb.debian.org` резолвится), исходящий трафик ходит — то есть изоляция режет именно доступ внутрь хоста, а не всё подряд. ## Про методику — почему первый заход не считается Сначала пробовал `</dev/tcp/HOST/PORT` через `lxc exec ... bash -c`. Все цели вернули 124, включая **контрольную** — а контроль обязан был пройти. Это значит, что не изоляция сработала, а проба не работала вовсе (`/dev/tcp` в этом bash недоступен), и любой вывод «изоляция держит» из тех цифр был бы выдуман. Переделал на `python3 socket.create_connection` с двумя контролями. Положительный контроль (`1.1.1.1:443`) прошёл — значит механика замера рабочая, и таймауты по docker-подсетям означают именно DROP. Отдельно: `10.100.0.1:22` (sshd хоста) из контейнера тоже недоступен — и это **намеренно**, в ufw явным правилом `22/tcp DENY from 10.100.0.0/24`. То есть зоопарк не сможет ходить по ssh на хост даже при компрометации WordPress. Это сверх того, что требует приёмка. ## Статус пункта - [x] **LXC изолирован: нет доступа к подсети Postgres, нет docker-сокета** — подтверждено пробами, не чтением конфигов. Оговорка: проверен контейнер `zoo` в текущем виде — пустой, без зоопарка. Когда в него приедут 51 юнит и WordPress, проверку надо **повторить той же пробой**: новые сервисы могут потребовать пробросов, и любой проброс (`devices`) способен эту изоляцию открыть. Команды выше воспроизводимы как есть.
Author
Owner

Бан отпустил, инвентарь снят. Ниже полный состав — и три расхождения с тем, что записано в задаче, одно из которых ломает заявленную схему изоляции.

Железо и ОС

Ubuntu 24.04.3 LTS, ядро 6.8.0-85, 8 CPU
RAM:    11 GiB всего / 3.2 GiB занято / 8.5 GiB доступно
диск /: 99 G всего / 36 G занято (37 %)
uptime: 303 дня

systemd

81 enabled / 52 running. В задаче сказано «51 юнит» — сходится.

Прикладные (22): agent-bot, agent-bot-max, akcii-web, avito-feed, bitrix-dashboard, crm-bot, crm-web, kartochki-web, kb-auth, mammoth-collector, mammoth-runner, prices, prinzip-ai, prinzip-api, prinzip-crm-external-api, prinzip-crm-sync-http, prinzip-knowledge-bot, prinzip-meeting-sync, prinzip-watcher, prinzip-web, promo-codes, razmetka, svg-processor.

Инфраструктурные: nginx, php8.3-fpm, mariadb, postgresql@16-main, docker, containerd, fail2ban, tailscaled, xray, zabbix-agent, qemu-guest-agent.

Сайты — 13 конфигов nginx

3inrow · analytics · crm · gifts · map · meeting · microbeam.ru · pattern · prinzip · prinzipevent.ru · progroup · svg · tz-generator (остальные — поддомены *.prinzipevent.ru).

Ровно 13, как в задаче.

Оговорка про мой же первый заход: grep по server_name нашёл только 6 — он был слишком узким (якорь на начало строки). Настоящий список — по именам файлов в sites-enabled/. Если кто-то будет сверять, не повторяйте мою ошибку.

Данные

Что Размер
/var/www 8.2 G
/root 9.0 G
/opt 6.5 G
MariaDB (progroup) 223 M
Postgres 16 (geowatch) 284 M

Базы: MariaDB progroup, Postgres geowatch. Всё остальное — файлы.

Рантаймы

node v22.21.0 · Python 3.12.3 · PHP 8.3.6 · MariaDB 10.11.14 · PostgreSQL 16.15 · PM2 6.0.14

PM2 держит 6 приложений: 3inrow-game, geowatch-api, gifts-app, testpk-monte, tower-bot, tower-game. Обращаю внимание на счётчик рестартов: gifts-app ↺ 89, tower-game ↺ 84, 3inrow-game ↺ 36 — эти приложения регулярно падают и поднимаются заново уже сейчас, до всякого переезда.

Cron: 10 записей у root + cron.d: certbot, kb-reminder, metka-backup, php, sysstat, e2scrub_all.


Три расхождения с планом

1. «Занимает ~4 ГБ» — это RAM, а не диск

3.2 GiB занятой памяти действительно ≈ 4 ГБ, тут сходится. Но на диске 36 ГБ, из них 24 ГБ — /var/www + /root + /opt. Планировать раздел под зоопарк надо от 36+ ГБ с запасом, а не от 4.

2. Сертификатов 9, а сайтов 13

/etc/letsencrypt/live/ содержит 9 имён: analytics, crm, gifts, map, meeting, microbeam.ru, prinzipevent.ru, progroup, svg (выпуск через certbot, acme.sh нет).

То есть у четырёх конфигов (3inrow, pattern, prinzip, tz-generator) отдельного сертификата нет. Возможных объяснений три: они отдаются под чужим сертификатом, они внутренние, или у них просто нет HTTPS. Пункт приёмки «13 сайтов отвечают 200 по HTTPS» сформулирован под предположение, которое пока не подтверждено — надо либо выяснить статус этих четырёх, либо переформулировать приёмку под фактическое число.

3. Docker внутри зоопарка против «LXC без docker-сокета» — конфликт

На сервере работает docker-контейнер koreana-app (Up 36 часов), плюс сам демон docker.service и containerd.service.

Задача требует: «LXC не видит подсеть Postgres, не имеет docker-сокета». Но чтобы koreana-app продолжил работать внутри LXC, там нужен свой docker-демон — а это вложенный docker, для которого контейнеру требуется security.nesting=true и, как правило, привилегированный режим. Ровно то, что заявленная изоляция должна исключать (я её проверял вчера на пустом zoo: devices: {}, сокета нет — и это держалось именно потому, что внутри ничего не требовало docker).

Развилка, и она за владельцем, потому что это выбор между безопасностью и объёмом работ:

  1. koreana-app переносится не в LXC, а в docker на хосте, рядом с остальным стеком — тогда изоляция зоопарка остаётся строгой, но приложение оказывается в одной docker-сети с ПДн-базами (то, чего изоляция и избегала).
  2. Включить security.nesting в LXC — зоопарк остаётся целиком в контейнере, но изоляция слабеет, а WordPress в задаче уже назван «типовой точкой входа».
  3. Переупаковать koreana-app в systemd-юнит без docker — чище всех, но это работа по приложению, и неизвестно, чьё оно.

Чего ещё нет в задаче, но приедет вместе с зоопарком

  • tailscaled — хост в Tailscale-сети (адрес 100.95.146.21). При переезде идентичность узла меняется: нужен повторный логин и, возможно, правка ACL. Если через Tailscale кто-то ходит в эти сервисы, после переезда доступ отвалится молча.
  • xray — прокси на порту 8752. Назначение из состава не определить; выяснить, чьё и нужно ли переносить, до гашения сервера.
  • zabbix-agent (порт 10050) — внешний мониторинг. Указывает на чей-то Zabbix-сервер, который после переезда будет опрашивать мёртвый адрес.
  • fail2ban — он и отрезал меня вчера при серии коротких переподключений. Учесть при переносе: правила бана приедут вместе с конфигом.

Статус приёмки после инвентаря

Пункт Статус
Зоопарк поднят в LXC, 51 юнит запущен состав известен, но упирается в развилку про docker
13 сайтов отвечают 200 по HTTPS требует DNS (#3027) и прояснения 4 сайтов без сертификата
LXC изолирован проверено (комментарий выше) — но проверять заново после переезда зоопарка, особенно если включат nesting
Цена старого сервера владелец
94.228.121.73 погашен владелец
Бан отпустил, инвентарь снят. Ниже полный состав — и **три расхождения с тем, что записано в задаче**, одно из которых ломает заявленную схему изоляции. ## Железо и ОС ``` Ubuntu 24.04.3 LTS, ядро 6.8.0-85, 8 CPU RAM: 11 GiB всего / 3.2 GiB занято / 8.5 GiB доступно диск /: 99 G всего / 36 G занято (37 %) uptime: 303 дня ``` ## systemd **81 enabled / 52 running.** В задаче сказано «51 юнит» — сходится. Прикладные (22): `agent-bot`, `agent-bot-max`, `akcii-web`, `avito-feed`, `bitrix-dashboard`, `crm-bot`, `crm-web`, `kartochki-web`, `kb-auth`, `mammoth-collector`, `mammoth-runner`, `prices`, `prinzip-ai`, `prinzip-api`, `prinzip-crm-external-api`, `prinzip-crm-sync-http`, `prinzip-knowledge-bot`, `prinzip-meeting-sync`, `prinzip-watcher`, `prinzip-web`, `promo-codes`, `razmetka`, `svg-processor`. Инфраструктурные: `nginx`, `php8.3-fpm`, `mariadb`, `postgresql@16-main`, `docker`, `containerd`, `fail2ban`, `tailscaled`, `xray`, `zabbix-agent`, `qemu-guest-agent`. ## Сайты — 13 конфигов nginx `3inrow` · `analytics` · `crm` · `gifts` · `map` · `meeting` · `microbeam.ru` · `pattern` · `prinzip` · `prinzipevent.ru` · `progroup` · `svg` · `tz-generator` (остальные — поддомены `*.prinzipevent.ru`). Ровно 13, как в задаче. > Оговорка про мой же первый заход: grep по `server_name` нашёл только 6 — он был слишком узким (якорь на начало строки). Настоящий список — по именам файлов в `sites-enabled/`. Если кто-то будет сверять, не повторяйте мою ошибку. ## Данные | Что | Размер | |---|---| | `/var/www` | 8.2 G | | `/root` | 9.0 G | | `/opt` | 6.5 G | | MariaDB (`progroup`) | 223 M | | Postgres 16 (`geowatch`) | 284 M | Базы: MariaDB `progroup`, Postgres `geowatch`. Всё остальное — файлы. ## Рантаймы node v22.21.0 · Python 3.12.3 · PHP 8.3.6 · MariaDB 10.11.14 · PostgreSQL 16.15 · PM2 6.0.14 PM2 держит 6 приложений: `3inrow-game`, `geowatch-api`, `gifts-app`, `testpk-monte`, `tower-bot`, `tower-game`. Обращаю внимание на счётчик рестартов: `gifts-app` ↺ 89, `tower-game` ↺ 84, `3inrow-game` ↺ 36 — эти приложения регулярно падают и поднимаются заново уже сейчас, до всякого переезда. Cron: 10 записей у root + `cron.d`: `certbot`, `kb-reminder`, `metka-backup`, `php`, `sysstat`, `e2scrub_all`. --- # Три расхождения с планом ## 1. «Занимает ~4 ГБ» — это RAM, а не диск 3.2 GiB занятой памяти действительно ≈ 4 ГБ, тут сходится. Но **на диске 36 ГБ**, из них 24 ГБ — `/var/www` + `/root` + `/opt`. Планировать раздел под зоопарк надо от 36+ ГБ с запасом, а не от 4. ## 2. Сертификатов 9, а сайтов 13 `/etc/letsencrypt/live/` содержит 9 имён: `analytics`, `crm`, `gifts`, `map`, `meeting`, `microbeam.ru`, `prinzipevent.ru`, `progroup`, `svg` (выпуск через certbot, `acme.sh` нет). То есть у четырёх конфигов (`3inrow`, `pattern`, `prinzip`, `tz-generator`) отдельного сертификата нет. Возможных объяснений три: они отдаются под чужим сертификатом, они внутренние, или у них просто нет HTTPS. **Пункт приёмки «13 сайтов отвечают 200 по HTTPS» сформулирован под предположение, которое пока не подтверждено** — надо либо выяснить статус этих четырёх, либо переформулировать приёмку под фактическое число. ## 3. Docker внутри зоопарка против «LXC без docker-сокета» — конфликт На сервере работает docker-контейнер **`koreana-app`** (Up 36 часов), плюс сам демон `docker.service` и `containerd.service`. Задача требует: *«LXC не видит подсеть Postgres, не имеет docker-сокета»*. Но чтобы `koreana-app` продолжил работать внутри LXC, там нужен **свой** docker-демон — а это вложенный docker, для которого контейнеру требуется `security.nesting=true` и, как правило, привилегированный режим. Ровно то, что заявленная изоляция должна исключать (я её проверял вчера на пустом `zoo`: `devices: {}`, сокета нет — и это держалось именно потому, что внутри ничего не требовало docker). Развилка, и она **за владельцем**, потому что это выбор между безопасностью и объёмом работ: 1. `koreana-app` переносится **не в LXC**, а в docker на хосте, рядом с остальным стеком — тогда изоляция зоопарка остаётся строгой, но приложение оказывается в одной docker-сети с ПДн-базами (то, чего изоляция и избегала). 2. Включить `security.nesting` в LXC — зоопарк остаётся целиком в контейнере, но изоляция слабеет, а WordPress в задаче уже назван «типовой точкой входа». 3. Переупаковать `koreana-app` в systemd-юнит без docker — чище всех, но это работа по приложению, и неизвестно, чьё оно. ## Чего ещё нет в задаче, но приедет вместе с зоопарком - **`tailscaled`** — хост в Tailscale-сети (адрес `100.95.146.21`). При переезде идентичность узла меняется: нужен повторный логин и, возможно, правка ACL. Если через Tailscale кто-то ходит в эти сервисы, после переезда доступ отвалится молча. - **`xray`** — прокси на порту `8752`. Назначение из состава не определить; выяснить, чьё и нужно ли переносить, до гашения сервера. - **`zabbix-agent`** (порт 10050) — внешний мониторинг. Указывает на чей-то Zabbix-сервер, который после переезда будет опрашивать мёртвый адрес. - **`fail2ban`** — он и отрезал меня вчера при серии коротких переподключений. Учесть при переносе: правила бана приедут вместе с конфигом. ## Статус приёмки после инвентаря | Пункт | Статус | |---|---| | Зоопарк поднят в LXC, 51 юнит запущен | состав известен, но **упирается в развилку про docker** | | 13 сайтов отвечают 200 по HTTPS | требует DNS (#3027) **и** прояснения 4 сайтов без сертификата | | LXC изолирован | ✅ проверено (комментарий выше) — но **проверять заново после переезда зоопарка**, особенно если включат nesting | | Цена старого сервера | владелец | | `94.228.121.73` погашен | владелец |
Author
Owner

Закрыл вопрос про «4 сайта без сертификата», который поднял в прошлом комментарии. Проверил не по конфигам, а снаружи — как их видит мир. Ответ короче, чем казалось: сайтов не 13, а 12, и два конфига мёртвые.

Замер: все хосты снаружи

Хост HTTPS HTTP Вердикт
prinzipevent.ru 200 живой
www.prinzipevent.ru 200 живой
3inrow.prinzipevent.ru 200 301 живой
analytics.prinzipevent.ru 200 301 живой
gifts.prinzipevent.ru 200 301 живой
map.prinzipevent.ru 200 301 живой
meeting.prinzipevent.ru 200 301 живой
progroup.prinzipevent.ru 200 301 живой
crm.prinzipevent.ru 302 301 живой (редирект на вход)
microbeam.ru 302 живой
www.microbeam.ru 302 живой
svg.prinzipevent.ru 401 301 живой (basic auth)
pattern.prinzipevent.ru NXDOMAIN
tz.prinzipevent.ru NXDOMAIN

Два конфига мёртвые, а не «без сертификата»

pattern и tz-generator — не «сайты, которым забыли выписать сертификат». У них:

  • нет DNS-записи вовсеnslookup отдаёт Non-existent domain с 1.1.1.1 для обоих;
  • нет listen 443 в конфиге (443:0);
  • нет ssl_certificate (ssl:0).

Три независимых признака сходятся: это остатки, а не работающие сайты. Переносить в LXC нечего, выпускать сертификаты не для чего.

Третий конфиг — тоже не сайт

sites-enabled/prinzip имеет server_name 94.228.121.73 — это catch-all на голый IP, с двумя ssl_certificate не из letsencrypt/live (то есть самоподписанный или snakeoil). Он ловит обращения по IP и не является публичным сайтом.

Поправка к моему же прошлому комментарию

Я написал, что сертификатов 9, а сайтов 13, и предположил, что четырём чего-то не хватает. Это было следствием обрезанного вывода ls /etc/letsencrypt/live/3inrow в него не попал, хотя сертификат у него есть (что и доказывает 200 по HTTPS). Реально сертификатов 10, и все они у живых доменов.

Как переформулировать приёмку

Сейчас в задаче: «13 сайтов отвечают 200 по HTTPS с новыми сертификатами». Ни одна половина не верна.

Предлагаю:

12 хостов отвечают по HTTPS с новыми сертификатами, с теми же кодами, что и до переезда:
200 — prinzipevent.ru, www.prinzipevent.ru, 3inrow, analytics, gifts, map, meeting, progroup
302 — crm, microbeam.ru, www.microbeam.ru
401 — svg

Почему не «200 у всех»: crm штатно редиректит на форму входа, svg закрыт basic-аутентификацией, microbeam.ru редиректит. Требовать от них 200 — значит либо провалить приёмку на здоровой системе, либо потом «подогнать» критерий, что хуже. Сверять надо с зафиксированным до переезда кодом, а не с абстрактным 200 — таблица выше и есть эта фиксация.

И отдельно:

pattern.prinzipevent.ru, tz.prinzipevent.ru — конфиги удалить, не переносить (NXDOMAIN, нет 443, нет сертификата)
sites-enabled/prinzip — catch-all на IP, переносить только если на новом хосте нужен такой же перехват обращений по адресу

Что это меняет по объёму

Немного, но в правильную сторону: два конфига из тринадцати переносить не нужно, а критерий приёмки перестаёт быть невыполнимым по построению.

Закрыл вопрос про «4 сайта без сертификата», который поднял в прошлом комментарии. Проверил не по конфигам, а снаружи — как их видит мир. Ответ короче, чем казалось: **сайтов не 13, а 12, и два конфига мёртвые**. ## Замер: все хосты снаружи | Хост | HTTPS | HTTP | Вердикт | |---|---|---|---| | `prinzipevent.ru` | **200** | | живой | | `www.prinzipevent.ru` | **200** | | живой | | `3inrow.prinzipevent.ru` | **200** | 301 | живой | | `analytics.prinzipevent.ru` | **200** | 301 | живой | | `gifts.prinzipevent.ru` | **200** | 301 | живой | | `map.prinzipevent.ru` | **200** | 301 | живой | | `meeting.prinzipevent.ru` | **200** | 301 | живой | | `progroup.prinzipevent.ru` | **200** | 301 | живой | | `crm.prinzipevent.ru` | **302** | 301 | живой (редирект на вход) | | `microbeam.ru` | **302** | | живой | | `www.microbeam.ru` | **302** | | живой | | `svg.prinzipevent.ru` | **401** | 301 | живой (basic auth) | | `pattern.prinzipevent.ru` | — | — | **NXDOMAIN** | | `tz.prinzipevent.ru` | — | — | **NXDOMAIN** | ## Два конфига мёртвые, а не «без сертификата» `pattern` и `tz-generator` — не «сайты, которым забыли выписать сертификат». У них: - **нет DNS-записи вовсе** — `nslookup` отдаёт `Non-existent domain` с 1.1.1.1 для обоих; - **нет `listen 443`** в конфиге (`443:0`); - **нет `ssl_certificate`** (`ssl:0`). Три независимых признака сходятся: это остатки, а не работающие сайты. Переносить в LXC нечего, выпускать сертификаты не для чего. ## Третий конфиг — тоже не сайт `sites-enabled/prinzip` имеет `server_name 94.228.121.73` — это **catch-all на голый IP**, с двумя `ssl_certificate` не из `letsencrypt/live` (то есть самоподписанный или snakeoil). Он ловит обращения по IP и не является публичным сайтом. ## Поправка к моему же прошлому комментарию Я написал, что сертификатов **9**, а сайтов **13**, и предположил, что четырём чего-то не хватает. Это было следствием обрезанного вывода `ls /etc/letsencrypt/live/` — `3inrow` в него не попал, хотя сертификат у него есть (что и доказывает 200 по HTTPS). Реально сертификатов **10**, и все они у живых доменов. ## Как переформулировать приёмку Сейчас в задаче: *«13 сайтов отвечают 200 по HTTPS с новыми сертификатами»*. Ни одна половина не верна. Предлагаю: > **12 хостов отвечают по HTTPS с новыми сертификатами, с теми же кодами, что и до переезда:** > 200 — `prinzipevent.ru`, `www.prinzipevent.ru`, `3inrow`, `analytics`, `gifts`, `map`, `meeting`, `progroup` > 302 — `crm`, `microbeam.ru`, `www.microbeam.ru` > 401 — `svg` Почему не «200 у всех»: `crm` штатно редиректит на форму входа, `svg` закрыт basic-аутентификацией, `microbeam.ru` редиректит. Требовать от них 200 — значит либо провалить приёмку на здоровой системе, либо потом «подогнать» критерий, что хуже. Сверять надо **с зафиксированным до переезда кодом**, а не с абстрактным 200 — таблица выше и есть эта фиксация. И отдельно: > `pattern.prinzipevent.ru`, `tz.prinzipevent.ru` — конфиги удалить, не переносить (NXDOMAIN, нет 443, нет сертификата) > `sites-enabled/prinzip` — catch-all на IP, переносить только если на новом хосте нужен такой же перехват обращений по адресу ## Что это меняет по объёму Немного, но в правильную сторону: два конфига из тринадцати переносить не нужно, а критерий приёмки перестаёт быть невыполнимым по построению.
Collaborator

Перепроверка 27.08.2026 (после пересборки базы 25-26.08 и переезда продукта на Poincare). Тело тикета писалось 20-24.08 по старой инфраструктуре.

Числа устарели кратно: сервисов в running 12, не 51; sites-enabled0, не 13. Старый хост 94.228.121.73 отвечает 301.

13 сертификатов, которые видит Caddy, — это домены gendsgn/mera, а не WP-зоопарк из тела. Пункт «перевыпустить сертификаты 13 сайтов» в исходной формулировке беспредметен.

**Перепроверка 27.08.2026** (после пересборки базы 25-26.08 и переезда продукта на Poincare). Тело тикета писалось 20-24.08 по старой инфраструктуре. Числа устарели кратно: сервисов в running **12**, не 51; `sites-enabled` — **0**, не 13. Старый хост `94.228.121.73` отвечает 301. 13 сертификатов, которые видит Caddy, — это домены gendsgn/mera, а не WP-зоопарк из тела. Пункт «перевыпустить сертификаты 13 сайтов» в исходной формулировке беспредметен.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3031
No description provided.