From c18a17495e7d227b9359875f653328fd06e24efb Mon Sep 17 00:00:00 2001 From: iAmScienceMan <63004048+iAmScienceMan@users.noreply.github.com> Date: Fri, 12 Jun 2026 15:03:24 +0300 Subject: [PATCH 1/2] =?UTF-8?q?docs:=20=D0=B0=D0=BA=D1=82=D1=83=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20DEPLOYMENT.md=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=20=D1=81=D0=B5=D0=BA=D1=80=D0=B5=D1=82-?= =?UTF-8?q?=D1=84=D0=B0=D0=B9=D0=BB=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D0=B0?= =?UTF-8?q?,=20hardening=20=D0=B8=20=D0=BF=D1=80=D0=B8=D0=BD=D1=8F=D1=82?= =?UTF-8?q?=D1=83=D1=8E=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D1=8C=20=D1=82?= =?UTF-8?q?=D0=B5=D0=B3=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 2 ++ docs/DEPLOYMENT.md | 69 ++++++++++++++++++++++++++++++++-------------- 2 files changed, 50 insertions(+), 21 deletions(-) diff --git a/.env.example b/.env.example index d148617..7b6620a 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,3 @@ TOKEN=your_discord_bot_token_here +# альтернатива: путь к файлу с токеном (docker secret), имеет приоритет над TOKEN +# TOKEN_FILE=/run/secrets/bot_token diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 25afe7f..def15c6 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -35,23 +35,58 @@ CD pipeline для kool-bot устроен так: В `/opt/kool-bot/`: - `docker-compose.yml` — host-specific конфиг, тянет образ из GHCR. В репу НЕ коммитится. -- `.env` — секреты бота (токен Discord и т.п.). В репу НЕ коммитится. -- `.last-known-good` — создаётся CD-скриптом, хранит имя предыдущего образа (для rollback). +- `data/` — состояние бота (json-файлы, heartbeat). Принадлежит `1000:1000`. +- `secrets/bot_token` — токен Discord одним файлом (без перевода строки). `0400`, владелец `1000:1000` — контейнер работает от uid 1000, а compose-секрет в не-swarm режиме это bind mount, который сохраняет права хостового файла. Контейнер должен называться `kool-bot` (`container_name: kool-bot` в compose). +Токен передаётся как docker secret: compose монтирует `secrets/bot_token` в `/run/secrets/bot_token`, а переменная `TOKEN_FILE=/run/secrets/bot_token` говорит боту читать его из файла (`config.py`). Раньше токен жил в `.env` и торчал в `/proc/1/environ` и `docker inspect`; `env_file` из compose убран. + Состояние бота (json-файлы) пишется в `DATA_DIR` (по умолчанию `/app/data`), куда монтируется volume `./data:/app/data`. Каталог должен быть доступен на запись пользователю `bot` (uid 1000). Раньше бот писал в `/app` и состояние стиралось при каждом пересоздании контейнера. +Сервис в compose захарднен: `read_only: true` (единственные записываемые пути — volume `/app/data` и tmpfs `/tmp`), `cap_drop: [ALL]`, `no-new-privileges:true`, `pids_limit`, `mem_limit`. При добавлении новых путей записи в коде это нужно учитывать. + +Опорный вид сервиса (host-конфиг, в репе не лежит): + +```yaml +services: + kool-bot: + image: ghcr.io/sys-class/kool-bot:latest + container_name: kool-bot + restart: unless-stopped + secrets: + - bot_token + environment: + - TOKEN_FILE=/run/secrets/bot_token + volumes: + - ./data:/app/data + read_only: true + tmpfs: + - /tmp + cap_drop: [ALL] + security_opt: + - no-new-privileges:true + pids_limit: 256 + mem_limit: 512m + +secrets: + bot_token: + file: ./secrets/bot_token +``` + ## Ручной деплой GitHub → Actions → CD → Run workflow. В поле `image_tag` нужно указать конкретный тег (SHA коммита) уже существующего образа. Ручной запуск НЕ пересобирает код — он только повторно раскатывает указанный образ (это и есть откат). Поэтому поле обязательное. -## Known limitations +## Модель тегов (принятое решение) + +Прод-`docker-compose.yml` намеренно остаётся на `image: ...:latest`. Локальный тег `latest` на хосте — это указатель, которым управляет только CD: перед `up` он тянет ровно собранный образ по неизменяемому digest (на push) или по SHA-тегу (на dispatch) и перевешивает на него локальный `latest`. Registry-side `latest` на хост не попадает. + +Альтернатива (compose закреплён на `@sha256:...`) отвергнута: тогда каждый деплой должен переписывать `image:` в host-конфиге, и рассинхрон CD со скриптом на хосте навсегда пиннил бы старый образ. -- `latest` на прод-хосте перевешивается CD на digest текущей раскатки. На стороне registry тег `latest` по-прежнему указывает на последнюю сборку из `main` — для воспроизводимости раскатки опирайся на SHA-тег/digest, а не на `latest`. -- Прод-`docker-compose.yml` тянет образ по тегу `latest` (CD ретегает его локально на нужный digest перед `up`). Полное закрепление по digest на стороне compose требует правок host-конфига в `/opt/kool-bot/` (вне репозитория). +Следствие: для воспроизводимости раскатки опирайся на SHA-тег/digest из CD-логов, а не на `latest` в registry. ## Откат @@ -65,28 +100,20 @@ GitHub → Actions → CD → Run workflow. ```bash ssh -p deploy@ cd /opt/kool-bot -docker compose down -# править docker-compose.yml: image: ghcr.io/sys-class/kool-bot: -docker compose pull -docker compose up -d +# перевесить локальный latest на нужный образ, как это делает CD +docker pull ghcr.io/sys-class/kool-bot: +docker tag ghcr.io/sys-class/kool-bot: ghcr.io/sys-class/kool-bot:latest +docker compose up -d --force-recreate docker logs kool-bot --tail 50 ``` -Либо использовать `.last-known-good`: - -```bash -cd /opt/kool-bot -PREV=$(cat .last-known-good) -sed -i "s|image: .*|image: $PREV|" docker-compose.yml -docker compose up -d -``` +CD при фейле healthcheck откатывается сам: id предыдущего образа он запоминает через `docker inspect` перед раскаткой. ## Добавление новых переменных окружения -1. На хосте: добавить переменную в `/opt/kool-bot/.env`. -2. Убедиться, что в `docker-compose.yml` сервис её подхватывает (`env_file: .env` или `environment:` секция). -3. Перезапустить: `cd /opt/kool-bot && docker compose up -d`. -4. Обновить `.env.example` в репе для документирования (без значения). +1. На хосте: добавить переменную в секцию `environment:` сервиса в `/opt/kool-bot/docker-compose.yml` (секреты — отдельными файлами через `secrets:`, по образцу `bot_token`). +2. Перезапустить: `cd /opt/kool-bot && docker compose up -d --force-recreate`. +3. Обновить `.env.example` в репе для документирования (без значения) — локально бот по-прежнему читает `.env`. Если переменная нужна на этапе сборки (build args) — добавлять через Dockerfile + workflow, не через `.env`. From 1616057d7b81dbc7d61362c69dc1e0c140c0af14 Mon Sep 17 00:00:00 2001 From: iAmScienceMan <63004048+iAmScienceMan@users.noreply.github.com> Date: Fri, 12 Jun 2026 15:15:32 +0300 Subject: [PATCH 2/2] =?UTF-8?q?ci:=20=D0=B2=D1=80=D0=B5=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=BE=D1=82=D0=BA=D0=BB=D1=8E=D1=87=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20gitleaks=20(=D0=BD=D1=83=D0=B6=D0=BD=D0=B0=20org-?= =?UTF-8?q?=D0=BB=D0=B8=D1=86=D0=B5=D0=BD=D0=B7=D0=B8=D1=8F=20GITLEAKS=5FL?= =?UTF-8?q?ICENSE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gitleaks-action для GitHub-организаций требует GITLEAKS_LICENSE и без него падает на проверке лицензии, не дойдя до сканирования. Отключаем до получения ключа; секреты пока сканирует trivy fs (scanners: secret). Co-Authored-By: Claude Opus 4.8 --- .github/workflows/security.yml | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 2ec7b7d..826d1f5 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -12,18 +12,22 @@ permissions: contents: read jobs: - secrets: - name: secret scan - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - with: - fetch-depth: 0 - - name: gitleaks - uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2 - env: - GITLEAKS_ENABLE_UPLOAD_ARTIFACT: "false" + # TODO: gitleaks-action требует GITLEAKS_LICENSE для GitHub-организаций и без + # него падает на проверке лицензии, не успев просканировать. Временно отключено + # до получения лицензионного ключа. Секрет-сканирование пока покрывает trivy fs + # ниже (scanners включают secret). + # secrets: + # name: secret scan + # runs-on: ubuntu-latest + # timeout-minutes: 10 + # steps: + # - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + # with: + # fetch-depth: 0 + # - name: gitleaks + # uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2 + # env: + # GITLEAKS_ENABLE_UPLOAD_ARTIFACT: "false" deps-and-fs: name: dependency / filesystem scan