[HIGH] ci: сервис-контейнер сборки с портом 5432 попадает в БОЕВОЙ Postgres — раннер работает в сети хоста, спасло только несовпадение пароля #2757

Open
opened 2026-08-06 21:03:01 +00:00 by bot-backend · 2 comments
Collaborator

Найдено при подключении живой БД к сборке (#2745). Первая версия конфигурации упала — и упала полезно, вскрыв то, что могло кончиться записью в боевую базу.

Что произошло

Стандартный приём — объявить services: postgres с публикацией порта 5432:5432 и ходить в localhost:5432. На этом раннере он работает не так:

  • раннер запускает и job, и сервис-контейнеры с --network host;
  • на том же VPS на 5432 уже слушает боевой Postgres.

Проверено сейчас:

ss -ltnp            → LISTEN 127.0.0.1:5432
docker ps           → gendesign-postgres-1   127.0.0.1:5432->5432/tcp

Сервис-контейнер занять порт не смог, psql из job'а ушёл в прод и получил password authentication failed for user "tradein".

Отказ произошёл только потому, что не совпали учётные данные. Совпади они — сборка выполняла бы миграции и тесты на боевой базе. Тесты этого сьюта создают и удаляют данные.

Почему это не разовая оплошность

Приём services: postgres + ports: 5432:5432 — самый очевидный и самый документированный способ дать сборке базу. Любой, кто будет добавлять БД в любой из четырёх workflow, напишет ровно так, и «оно заработает»: подключение пройдёт, тесты позеленеют, а писать они будут не туда, куда автор думает.

Дополнительная ловушка оттуда же: pg_isready через unix-сокет зеленеет на временном сервере фазы initdb — то есть готовность подтверждается до того, как настоящий сервер поднялся. Проверка готовности должна идти по TCP.

Как сделано сейчас (#2745)

Контейнер поднимается явным docker run в bridge-сети без публикации порта, строка подключения собирается из его IP. Проба готовности — по TCP.

Что предлагается сверх этого

  1. Сделать ловушку невозможной, а не обойдённой. Сейчас правильный способ записан в одном месте; следующий автор про него не узнает. Минимум — комментарий в каждом workflow рядом с местом, где напрашивается services:. Лучше — проверка, роняющая сборку, если в job'е объявлен сервис с публикацией 5432.
  2. Проверить, нет ли той же коллизии по другим портам — на VPS живут Redis, CouchDB, GlitchTip и сам Forgejo. Любой сервис-контейнер сборки с публикацией их порта попадёт в ту же ситуацию.
  3. Отдельно стоит спросить владельца, нужен ли боевому Postgres публикуемый порт на хосте вообще: tradein-postgres его не публикует (5432/tcp без маппинга) и прекрасно работает — соседние контейнеры ходят по имени в общей сети. Публикация на 127.0.0.1 защищает от внешнего мира, но не от процессов на самой машине, а раннер — как раз такой процесс.

Пункт 3 закрывает причину, первые два — следствие.

Связано: #2722, #2745.

Найдено при подключении живой БД к сборке (#2745). Первая версия конфигурации упала — и упала **полезно**, вскрыв то, что могло кончиться записью в боевую базу. ## Что произошло Стандартный приём — объявить `services: postgres` с публикацией порта `5432:5432` и ходить в `localhost:5432`. На этом раннере он работает не так: - раннер запускает и job, и сервис-контейнеры с `--network host`; - на том же VPS **на 5432 уже слушает боевой Postgres**. Проверено сейчас: ``` ss -ltnp → LISTEN 127.0.0.1:5432 docker ps → gendesign-postgres-1 127.0.0.1:5432->5432/tcp ``` Сервис-контейнер занять порт не смог, `psql` из job'а ушёл **в прод** и получил `password authentication failed for user "tradein"`. **Отказ произошёл только потому, что не совпали учётные данные.** Совпади они — сборка выполняла бы миграции и тесты на боевой базе. Тесты этого сьюта создают и удаляют данные. ## Почему это не разовая оплошность Приём `services: postgres` + `ports: 5432:5432` — самый очевидный и самый документированный способ дать сборке базу. Любой, кто будет добавлять БД в любой из четырёх workflow, напишет ровно так, и «оно заработает»: подключение пройдёт, тесты позеленеют, а писать они будут не туда, куда автор думает. Дополнительная ловушка оттуда же: **`pg_isready` через unix-сокет зеленеет на временном сервере фазы initdb** — то есть готовность подтверждается до того, как настоящий сервер поднялся. Проверка готовности должна идти по TCP. ## Как сделано сейчас (#2745) Контейнер поднимается явным `docker run` в **bridge**-сети **без публикации порта**, строка подключения собирается из его IP. Проба готовности — по TCP. ## Что предлагается сверх этого 1. **Сделать ловушку невозможной, а не обойдённой.** Сейчас правильный способ записан в одном месте; следующий автор про него не узнает. Минимум — комментарий в каждом workflow рядом с местом, где напрашивается `services:`. Лучше — проверка, роняющая сборку, если в job'е объявлен сервис с публикацией 5432. 2. **Проверить, нет ли той же коллизии по другим портам** — на VPS живут Redis, CouchDB, GlitchTip и сам Forgejo. Любой сервис-контейнер сборки с публикацией их порта попадёт в ту же ситуацию. 3. **Отдельно стоит спросить владельца**, нужен ли боевому Postgres публикуемый порт на хосте вообще: `tradein-postgres` его не публикует (`5432/tcp` без маппинга) и прекрасно работает — соседние контейнеры ходят по имени в общей сети. Публикация на `127.0.0.1` защищает от внешнего мира, но не от процессов на самой машине, а раннер — как раз такой процесс. Пункт 3 закрывает причину, первые два — следствие. Связано: #2722, #2745.
Author
Collaborator

Пункты 1-2 сделаны в PR #2759 (гейт scripts/check-workflow-ports.py + снятый капкан в шапке ci.yml). Пункт 3 не трогаю — это решение владельца, ниже факты и вопрос.

Что показал замер (ss -ltnp + docker ps, bot-server, 2026-08-06)

Занято на хосте: 22 (sshd), 53 (systemd-resolved), 80/443 (caddy), 2222 (forgejo ssh), 3000 (gendesign-frontend), 5432 (gendesign-postgres), 8000 (gendesign-backend).

Предпосылка пункта 2 issue частично не подтвердилась: Redis, CouchDB и GlitchTip на хосте НЕ слушают — в docker ps у них 6379/tcp, 5984/tcp, 8000/tcp без ->, они в bridge-сетях. Сервис-контейнер сборки с этими портами ни с чем не столкнётся.

Зато нашёлся порт, которого в issue не было: 8000 — боевой API. Ловушка ровно та же (services: ... ports: 8000:8000 → job уходит в прод), но там нет даже пароля, который спас в случае с БД.

Вопрос по пункту 3

Публикация 127.0.0.1:5432 не бесполезна — она несущая для доступа к БД по ssh-туннелю: в ~/.ssh/config стоит LocalForward 15432 127.0.0.1:5432LocalForward 8000 127.0.0.1:8000). Просто убрать публикацию, как у tradein-postgres, значит сломать ssh -N gendesign + psql на localhost:15432. Поэтому сравнение с соседним контейнером неполное: у соседа такого потребителя нет.

Варианты, решать вам:

  1. Оставить как есть. Конкретный способ выстрелить теперь ловится гейтом из #2759. Остаточный риск: любой процесс на машине (job раннера, ручной psql) по localhost:5432 попадает в прод, и защищает только пароль.
  2. Сдвинуть публикацию на нестандартный порт: в docker-compose.prod.yml 127.0.0.1:15432->5432 вместо 127.0.0.1:5432->5432, в ~/.ssh/configLocalForward 15432 127.0.0.1:15432. Туннель работает как раньше, а хостовый 5432 освобождается: тогда наивный services: postgres + ports: 5432:5432 в сборке начинает делать ровно то, что автор думает — поднимать ТЕСТОВУЮ базу. Ловушка не обходится и не запрещается, а перестаёт существовать. Цена: одна строка в compose, одна в ssh-конфиге, пересоздание контейнера БД (простой ~10с) и сверка, что больше никто не ходит в 5432 на хосте.
  3. Убрать публикацию совсем и ходить в БД через docker exec psql / IP контейнера. Дешевле всего в конфиге, но ломает привычный туннель и GUI-клиенты.

Мой голос за (2) — он единственный убирает причину, а не следствие. Но это правка боевого compose + вашего ssh-конфига, поэтому без вашего слова не делаю.

Побочно: .github/workflows/ci.yml — мёртвый файл (GitHub Actions у нас не исполняется, последняя правка 16.05), и именно в нём лежит эталонный анти-пример ports: 5432:5432, на который до #2759 указывал комментарий в .forgejo/workflows/ci.yml. Гейт его не сканирует намеренно (там он безвреден). Удалить его как источник копипасты — тоже ваше решение.

Пункты 1-2 сделаны в PR #2759 (гейт `scripts/check-workflow-ports.py` + снятый капкан в шапке `ci.yml`). Пункт 3 не трогаю — это решение владельца, ниже факты и вопрос. ## Что показал замер (`ss -ltnp` + `docker ps`, bot-server, 2026-08-06) Занято на хосте: **22** (sshd), **53** (systemd-resolved), **80/443** (caddy), **2222** (forgejo ssh), **3000** (gendesign-frontend), **5432** (gendesign-postgres), **8000** (gendesign-backend). Предпосылка пункта 2 issue частично **не подтвердилась**: Redis, CouchDB и GlitchTip на хосте НЕ слушают — в `docker ps` у них `6379/tcp`, `5984/tcp`, `8000/tcp` без `->`, они в bridge-сетях. Сервис-контейнер сборки с этими портами ни с чем не столкнётся. Зато нашёлся порт, которого в issue не было: **8000 — боевой API**. Ловушка ровно та же (`services: ... ports: 8000:8000` → job уходит в прод), но там нет даже пароля, который спас в случае с БД. ## Вопрос по пункту 3 Публикация `127.0.0.1:5432` не бесполезна — она несущая для доступа к БД по ssh-туннелю: в `~/.ssh/config` стоит `LocalForward 15432 127.0.0.1:5432` (и `LocalForward 8000 127.0.0.1:8000`). Просто убрать публикацию, как у `tradein-postgres`, значит сломать `ssh -N gendesign` + psql на `localhost:15432`. Поэтому сравнение с соседним контейнером неполное: у соседа такого потребителя нет. Варианты, решать вам: 1. **Оставить как есть.** Конкретный способ выстрелить теперь ловится гейтом из #2759. Остаточный риск: любой процесс на машине (job раннера, ручной psql) по `localhost:5432` попадает в прод, и защищает только пароль. 2. **Сдвинуть публикацию на нестандартный порт**: в `docker-compose.prod.yml` `127.0.0.1:15432->5432` вместо `127.0.0.1:5432->5432`, в `~/.ssh/config` — `LocalForward 15432 127.0.0.1:15432`. Туннель работает как раньше, а хостовый 5432 освобождается: тогда наивный `services: postgres` + `ports: 5432:5432` в сборке начинает делать ровно то, что автор думает — поднимать ТЕСТОВУЮ базу. Ловушка не обходится и не запрещается, а перестаёт существовать. Цена: одна строка в compose, одна в ssh-конфиге, пересоздание контейнера БД (простой ~10с) и сверка, что больше никто не ходит в 5432 на хосте. 3. **Убрать публикацию совсем** и ходить в БД через `docker exec psql` / IP контейнера. Дешевле всего в конфиге, но ломает привычный туннель и GUI-клиенты. Мой голос за (2) — он единственный убирает причину, а не следствие. Но это правка боевого compose + вашего ssh-конфига, поэтому без вашего слова не делаю. Побочно: `.github/workflows/ci.yml` — мёртвый файл (GitHub Actions у нас не исполняется, последняя правка 16.05), и именно в нём лежит эталонный анти-пример `ports: 5432:5432`, на который до #2759 указывал комментарий в `.forgejo/workflows/ci.yml`. Гейт его не сканирует намеренно (там он безвреден). Удалить его как источник копипасты — тоже ваше решение.
Author
Collaborator

Пункты 1-2 закрыты и ПРОВЕРЕНЫ НА СЛОМ. Пункт 3 — за владельцем, задача остаётся открытой.

Гейт #2759 краснеет на заведомо плохом входе — проверил сам

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

Скопировал .forgejo/workflows/ целиком, прогнал гейт как есть:

✓ ни один workflow не публикует занятый на VPS порт        EXIT=0

Затем вписал в копию настоящего ci-tradein.yml ровно тот приём, ради которого задача заводилась (services: postgres + ports: - 5432:5432) и прогнал снова:

::error file=.forgejo/workflows/ci-tradein.yml,line=34::публикация порта 5432 — он
занят на VPS (gendesign-postgres-1 — БОЕВАЯ БД (127.0.0.1:5432)). Раннер работает
в сети хоста: контейнер порт не займёт, а job уйдёт в этот прод-сервис (#2757).
Поднимай сервис через `docker run` в bridge-сети БЕЗ публикации и ходи по IP
контейнера — образец в .forgejo/workflows/ci-tradein.yml. || - 5432:5432
EXIT=1

Отдельно проверил форму docker run -p 8000:8000 (боевой API, где нет даже пароля) — тоже краснеет. И проверил, что на безопасной форме (docker run без публикации + подключение по IP контейнера) гейт молчит: сторож, краснеющий на правильном коде, потребовали бы выключить.

Сообщение называет файл, строку, порт, кто его занял и что делать вместо — то есть следующий автор узнает про ловушку в момент, когда в неё шагнёт.

Гейт вшит шагом в job changes (ci.yml:59-66), который бежит на каждом PR, и запускается вместе с самотестом (--selftest), так что порча самого детектора тоже роняет сборку.

Вывод: сторож различает два состояния конфигурации, а не подтверждает сам себя.

Пункт 2 — предпосылка задачи была частично неверна, и это уже исправлено в треде

Redis / CouchDB / GlitchTip на хосте не слушают. Зато нашёлся не упомянутый в задаче порт 8000 — боевой API, где ловушка та же, а пароля, спасшего в случае с БД, нет вовсе. Он в списке гейта.

Пункт 3 — открыт, и это единственное, что осталось

Вопрос владельцу задан 2026-08-06 21:27 и ответа не получил: нужна ли боевому Postgres публикация 127.0.0.1:5432 на хосте, с учётом того, что на ней держится LocalForward в ~/.ssh/config.

Пока публикация есть, ловушка обойдена, но не устранена: любой процесс на машине, попавший на localhost:5432, приходит в прод, и защищает только пароль. Гейт закрывает конкретный способ выстрелить в сборке; он не закрывает причину.

Задача остаётся открытой до решения по варианту (1/2/3) из комментария выше. Побочно там же ждёт решения .github/workflows/ci.yml — мёртвый файл, который при этом служит эталонным анти-примером для копипасты.

## Пункты 1-2 закрыты и ПРОВЕРЕНЫ НА СЛОМ. Пункт 3 — за владельцем, задача остаётся открытой. ### Гейт #2759 краснеет на заведомо плохом входе — проверил сам Наличие сторожа ничего не значит: вчера в этом же репозитории нашли сторожа, бравшего ожидание из той же настройки, которую он охраняет. Поэтому проверял не существование, а поведение — на **настоящем** файле воркфлоу, а не на выдуманном. Скопировал `.forgejo/workflows/` целиком, прогнал гейт как есть: ``` ✓ ни один workflow не публикует занятый на VPS порт EXIT=0 ``` Затем вписал в копию **настоящего** `ci-tradein.yml` ровно тот приём, ради которого задача заводилась (`services: postgres` + `ports: - 5432:5432`) и прогнал снова: ``` ::error file=.forgejo/workflows/ci-tradein.yml,line=34::публикация порта 5432 — он занят на VPS (gendesign-postgres-1 — БОЕВАЯ БД (127.0.0.1:5432)). Раннер работает в сети хоста: контейнер порт не займёт, а job уйдёт в этот прод-сервис (#2757). Поднимай сервис через `docker run` в bridge-сети БЕЗ публикации и ходи по IP контейнера — образец в .forgejo/workflows/ci-tradein.yml. || - 5432:5432 EXIT=1 ``` Отдельно проверил форму `docker run -p 8000:8000` (боевой API, где нет даже пароля) — тоже краснеет. И проверил, что на безопасной форме (`docker run` без публикации + подключение по IP контейнера) гейт молчит: сторож, краснеющий на правильном коде, потребовали бы выключить. Сообщение называет файл, строку, порт, кто его занял и что делать вместо — то есть следующий автор узнает про ловушку в момент, когда в неё шагнёт. Гейт вшит шагом в job `changes` (`ci.yml:59-66`), который бежит на каждом PR, и запускается вместе с самотестом (`--selftest`), так что порча самого детектора тоже роняет сборку. **Вывод: сторож различает два состояния конфигурации, а не подтверждает сам себя.** ### Пункт 2 — предпосылка задачи была частично неверна, и это уже исправлено в треде Redis / CouchDB / GlitchTip на хосте не слушают. Зато нашёлся не упомянутый в задаче порт **8000 — боевой API**, где ловушка та же, а пароля, спасшего в случае с БД, нет вовсе. Он в списке гейта. ### Пункт 3 — открыт, и это единственное, что осталось Вопрос владельцу задан 2026-08-06 21:27 и ответа не получил: нужна ли боевому Postgres публикация `127.0.0.1:5432` на хосте, с учётом того, что на ней держится `LocalForward` в `~/.ssh/config`. Пока публикация есть, ловушка **обойдена, но не устранена**: любой процесс на машине, попавший на `localhost:5432`, приходит в прод, и защищает только пароль. Гейт закрывает конкретный способ выстрелить в сборке; он не закрывает причину. Задача остаётся открытой до решения по варианту (1/2/3) из комментария выше. Побочно там же ждёт решения `.github/workflows/ci.yml` — мёртвый файл, который при этом служит эталонным анти-примером для копипасты.
lekss361 added the
bug
ci
scope/devops
security
labels 2026-08-16 10:25:18 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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#2757
No description provided.