Compliance-doc: реестр классов секретов (CI/CD + prod runtime), процедуры ротации per-class с downtime-эффектом, периодичность, quarterly audit-чеклист. OIDC verdict: future (Forgejo 10.0.3 без Actions OIDC token provider; deploy = прямой SSH, не cloud federation). Флагает sshkey.txt (PR #391) достижимый в git-истории — deploy-key скомпрометирован, нужна ротация. Значения — в vault meta/00_credentials.md (не в репо). Closes #78
24 KiB
Secrets Rotation Policy & Procedure
Issue: #78 (B6-4) · Owner: devops-engineer · Created: 2026-06-13
Версионированная (in-repo) копия политики ротации секретов. Источник истины по
значениям секретов — Obsidian vault meta/00_credentials.md (вне репозитория,
владелец — Anton). Этот документ описывает классы секретов (имена, расположение,
процедуру ротации, downtime-эффект, периодичность) — без значений.
⚠️ Этот файл НЕ содержит и не должен содержать значений секретов. Только имена переменных, их расположение и процедуры. PR-ревью отклоняет любой diff с literal-секретом (
.claude/rules/git-pr.md→ security tripwire).
0. Модель хранения секретов
| Хранилище | Что лежит | В git? | Кто читает |
|---|---|---|---|
Forgejo repo secrets (lekss361/gendesign → Settings → Actions → Secrets) |
CI/CD: deploy SSH key, GHCR PAT, bot-токены, DSN, прод API-ключи прокидываемые в .env.runtime |
❌ нет | Forgejo Actions runner |
Forgejo repo variables (vars.*) |
non-sensitive toggles (LLM_ENABLED, OWN_DEVELOPER_IDS) |
❌ нет | Forgejo Actions runner |
GitHub repo secrets (зеркало для .github/workflows/) |
deploy SSH key (obsidian-стек) | ❌ нет | GitHub Actions (только obsidian deploy) |
/opt/gendesign/.env (VPS, root-only, chmod 600) |
DB creds, GlitchTip infra-secrets, FDW/reader passwords, прокси, COMPOSE_PROFILES | ❌ .gitignore |
docker compose (main + obsidian + tradein стеки) |
/opt/gendesign/backend/.env.runtime (VPS, chmod 600) |
runtime overlay: SENTRY_RELEASE, GLITCHTIP_DSN, OBJECTIVE_API_KEY, OPENAI_API_KEY, OWN_DEVELOPER_IDS, GENDESIGN_FDW_PASSWORD, COUCHDB_* |
❌ .gitignore |
backend/worker/beat/couchdb |
/opt/gendesign/tradein-mvp/backend/.env.runtime (VPS, chmod 600) |
tradein DB creds, Yandex/DaData ключи, прокси-URL, Cian-логин, reader password | ❌ .gitignore |
tradein стек |
caddy/users.caddy.snippet (in git) |
bcrypt-хеши basic_auth пилотных юзеров | ✅ да (хеши, не plaintext) | Caddy |
Obsidian vault meta/00_credentials.md |
реестр значений всех секретов + audit-log ротаций | ❌ (вне репо) | Anton |
Правило редактирования .env / .env.runtime: только in-place (sed/append).
Полная перезапись файла затрёт user-managed секреты, которых нет в репо. См.
.claude/rules/deploy.md → «Полностью перезаписывать .env.runtime — только sed».
Правило применения изменений: docker compose restart НЕ перечитывает env_file:.
После правки env — docker compose -p <project> -f <file> up -d --force-recreate --no-deps <svc>.
1. Реестр классов секретов
Сгруппировано по классу. Колонка «Ротация» ссылается на процедуру в §2.
1.1 CI/CD (Forgejo / GitHub repo secrets)
| Имя | Где живёт | Назначение | Класс ротации |
|---|---|---|---|
DEPLOY_SSH_KEY |
Forgejo secrets + GitHub secrets | Приватный SSH-ключ для appleboy/ssh-action → VPS deploy. Используется в deploy.yml, deploy-tradein.yml, deploy-obsidian.yml |
A (SSH key) |
DEPLOY_HOST / DEPLOY_USER / DEPLOY_PORT |
Forgejo + GitHub secrets | Адрес/юзер/порт VPS. Не «секрет» в строгом смысле, но хранится как secret | A |
GHCR_PAT |
Forgejo secrets | GitHub Container Registry PAT (docker login ghcr.io -u lekss361). Push образов + pull на VPS |
B (PAT) |
FORGEJO_BOT_QA_TOKEN |
Forgejo secrets | bot-qa PAT (write:issue) для stale-claims.yml / scripts/cleanup-stale-claims.sh |
B |
GLITCHTIP_BACKEND_DSN |
Forgejo secrets | Backend Sentry DSN → пишется в .env.runtime на deploy |
C (DSN) |
GLITCHTIP_FRONTEND_DSN |
Forgejo secrets | Build-arg NEXT_PUBLIC_GLITCHTIP_DSN для frontend образа |
C |
OBJECTIVE_API_KEY |
Forgejo secrets | Объектив live-scraper (sync_all_groups, #307). Пустой = no-op |
D (3rd-party API key) |
OPENAI_API_KEY |
Forgejo secrets | LLM-чат (#960/#957). UNSET по умолчанию → фича dormant | D |
Дополнительно во вне-репозиторном
FORGEJO_TOKEN(личный PAT Anton) живёт доступ к Forgejo API из CLI/скриптов. Класс B.
1.2 Прод runtime — main стек (/opt/gendesign/.env + backend/.env.runtime)
| Имя | Файл | Назначение | Класс ротации |
|---|---|---|---|
POSTGRES_PASSWORD |
.env |
Пароль роли gendesign (PostGIS 16) |
E (DB password) |
POSTGRES_USER / POSTGRES_DB |
.env |
Имя роли / БД (не секрет, но в .env) |
E |
GENDESIGN_FDW_PASSWORD |
backend/.env.runtime |
Пароль роли tradein_fdw_reader (FDW из main → tradein). Применяется через ops/db-bootstrap/set_tradein_fdw_password.sql |
E |
COUCHDB_PASSWORD / COUCHDB_USER |
backend/.env.runtime |
CouchDB (Obsidian LiveSync, obsidian.gendsgn.ru) |
E |
GLITCHTIP_DSN |
backend/.env.runtime |
Backend GlitchTip DSN (перезаписывается deploy из GLITCHTIP_BACKEND_DSN) |
C |
GLITCHTIP_DB_PASS |
.env |
Пароль БД GlitchTip-стека | E |
GLITCHTIP_SECRET |
.env |
Django SECRET_KEY GlitchTip |
F (app secret) |
OBJECTIVE_API_KEY |
backend/.env.runtime |
Зеркало CI-секрета на VPS | D |
OPENAI_API_KEY |
backend/.env.runtime |
Зеркало CI-секрета (только если non-empty) | D |
SCRAPE_ADMIN_TOKEN |
backend/.env |
DEPRECATED (PR #436): app-level admin auth удалён, заменён Caddy basic_auth. Поле оставлено в core/deps.py для быстрого rollback |
F (legacy, см. §3) |
1.3 Прод runtime — tradein стек (/opt/gendesign/tradein-mvp/backend/.env.runtime)
| Имя | Назначение | Класс ротации |
|---|---|---|
TRADEIN_POSTGRES_PASSWORD / TRADEIN_POSTGRES_USER |
Пароль/юзер БД tradein |
E |
TRADEIN_READER_PASSWORD |
Пароль роли gendesign_reader (ETL #976, ops/db-bootstrap/set_gendesign_reader_password.sql) |
E |
YANDEX_GEOCODER_API_KEY |
Yandex Geocoder (25k req/day) | D |
DADATA_API_TOKEN / DADATA_API_SECRET |
DaData /clean/address enrichment |
D |
SCRAPER_PROXY_URL (+ legacy AVITO_PROXY_URL, CIAN_PROXY_URL, YANDEX_PROXY_URL и их *_ROTATE_URL) |
Мобильный прокси для скраперов (содержит user:pass в URL) | G (proxy creds) |
CIAN_LOGIN_EMAIL / CIAN_LOGIN_PASSWORD |
Cian browser auto-login (#639, Variant B) | D |
COOKIE_ENCRYPTION_KEY |
Шифрование cookie-стейта браузера | F |
1.4 Proxy / basic_auth (Caddy)
| Артефакт | Где | Назначение | Класс ротации |
|---|---|---|---|
caddy/users.caddy.snippet |
in git | bcrypt-хеши пилотных юзеров (basic_auth gate, realm GenDesign Pilot) |
H (basic_auth) |
bcrypt-хеши — односторонние, не plaintext-секреты, поэтому намеренно живут в репо.
Ротация полностью описана в docs/runbooks/basic_auth_management.md.
2. Процедуры ротации по классам
Общие принципы:
- Любая ротация = vault entry в
meta/00_credentials.md(audit-log: что, когда, кто). - Меняем
.env/.env.runtimeтолькоsed/append, не перезаписываем файл целиком. - После правки runtime-env прод-сервис поднимаем через
up -d --force-recreate --no-deps <svc>(compose project:gendesign/gendesign-tradein/gendesign-obsidian).
Класс A — Deploy SSH key (DEPLOY_SSH_KEY + host/user/port)
Downtime: нет (ключ используется только во время CI deploy-шага).
- На VPS под deploy-юзером сгенерировать новую keypair:
ssh-keygen -t ed25519 -C "forgejo-deploy-$(date +%Y%m%d)" -f ~/.ssh/forgejo_deploy_new. - Добавить новый публичный ключ в
~/.ssh/authorized_keysна VPS (рядом со старым — оба валидны на время cutover). - Обновить secret
DEPLOY_SSH_KEY(приватная часть) в обоих местах: Forgejo repo secrets и GitHub repo secrets (obsidian-стек использует GitHub Actions). - Прогнать
workflow_dispatchнаdeploy.yml— убедиться, что deploy зелёный с новым ключом. - Удалить старый публичный ключ из
authorized_keys. - Vault entry.
⚠️ КРИТИЧНО (исторический инцидент): файл
sshkey.txtбыл закоммичен и удалён в PR #391 (commit6f37240), но остаётся в git-истории — этот deploy-ключ следует считать скомпрометированным и ротировать по процедуре выше как приоритетную задачу (если ещё не сделано). Удаление файла из tree НЕ убирает blob из истории; чистка истории (git filter-repo) — отдельная операция, но ротация ключа важнее (после ротации старый blob бесполезен).
Класс B — PAT (GHCR_PAT, FORGEJO_BOT_QA_TOKEN, FORGEJO_TOKEN)
Downtime: нет (если новый PAT выпущен до отзыва старого).
- Выпустить новый PAT в соответствующем сервисе:
GHCR_PAT: GitHub → Settings → Developer settings → Tokens (classic) сwrite:packages,read:packages.FORGEJO_BOT_QA_TOKEN: войти под bot-qa в Forgejo → Settings → Applications → minimumwrite:issue.FORGEJO_TOKEN: личный PAT Anton (Forgejo → Applications).
- Обновить значение в Forgejo repo secrets (для CI) / в keychain (для CLI).
- Триггернуть зависимый workflow (
deploy.ymlдля GHCR,stale-claims.ymlдля bot-qa) — проверить зелёный. - Отозвать старый PAT в сервисе.
- Vault entry.
deploy.yml/deploy-tradein.ymlделаютdocker login ghcr.ioповторно на каждом deploy, так что ротацияGHCR_PATподхватывается со следующим деплоем без ручного вмешательства на VPS.
Класс C — GlitchTip DSN (GLITCHTIP_BACKEND_DSN, GLITCHTIP_FRONTEND_DSN, GLITCHTIP_DSN)
Downtime: нет (пустой DSN = SDK no-op, события просто не шлются в окно ротации).
- В GlitchTip UI (
errors.gendsgn.ru) → Project Settings → Client Keys (DSN) → revoke + create new. - Backend: обновить
GLITCHTIP_BACKEND_DSNв Forgejo secrets → deploy пропишет его в.env.runtimeчерезsedи--force-recreatebackend/worker/beat. - Frontend: обновить
GLITCHTIP_FRONTEND_DSN(build-argNEXT_PUBLIC_GLITCHTIP_DSN) → требует rebuild frontend образа (запекается на build-time) →workflow_dispatchили push вfrontend/**. - Vault entry.
Класс D — 3rd-party API keys (OBJECTIVE_API_KEY, OPENAI_API_KEY, YANDEX_GEOCODER_API_KEY, DADATA_*, CIAN_LOGIN_*)
Downtime: нет (фичи gracefully degrade при пустом ключе — см. config-комментарии).
- Перевыпустить/ротировать ключ в кабинете провайдера (Объектив / OpenAI / Yandex Cloud / DaData / Cian-аккаунт).
- Где живёт:
OBJECTIVE_API_KEY,OPENAI_API_KEY— Forgejo secret → deploy пишет в main.env.runtime.YANDEX_GEOCODER_API_KEY,DADATA_*,CIAN_LOGIN_*— tradein.env.runtime(правится на VPS вручную, не из CI).
- Обновить значение
sed-ом (НЕ перезапись файла) иup -d --force-recreate --no-deps backend worker beat(main) /... backend scraper(tradein). - Vault entry.
Класс E — DB passwords (POSTGRES_PASSWORD, TRADEIN_POSTGRES_PASSWORD, *_FDW_PASSWORD, *_READER_PASSWORD, COUCHDB_PASSWORD, GLITCHTIP_DB_PASS)
Downtime: короткий — backend переподключается после смены (несколько секунд).
Главная роль (gendesign / tradein):
- Сгенерировать пароль:
openssl rand -base64 24 | tr -d '/+' | head -c 24. ALTER ROLE <role> WITH PASSWORD '<new>';внутри контейнера postgres (docker compose -p <project> exec postgres psql -U <user> -d <db> -c "ALTER ROLE ...").- Обновить пароль в
.env(POSTGRES_PASSWORD) /.env.runtimesed-ом. up -d --force-recreate --no-deps backend worker beat(иcaddy/frontendесли завязаны).- Vault entry.
Bootstrap-роли (tradein_fdw_reader, gendesign_reader):
- Пароль живёт ТОЛЬКО в
.env.runtime; роль создаётся passwordless SQL-миграцией. - Меняем переменную (
GENDESIGN_FDW_PASSWORD/TRADEIN_READER_PASSWORD) в.env.runtime, затем повторно прогоняем idempotent-скриптops/db-bootstrap/set_*_password.sql(deploy.yml/deploy-tradein.yml делают это автоматически на следующем деплое).
COUCHDB_PASSWORD: сменить в CouchDB (_users / admin) + backend/.env.runtime + up -d --force-recreate couchdb через docker-compose.obsidian.yml. Все LiveSync-клиенты потребуют новый пароль.
Класс F — App secrets (GLITCHTIP_SECRET, COOKIE_ENCRYPTION_KEY, JWT_SECRET)
Downtime / эффект: смена инвалидирует выпущенные артефакты (сессии/токены/cookie).
- Сгенерировать:
openssl rand -hex 32(или-base64 48). - Обновить в соответствующем
.env/.env.runtime. up -d --force-recreateзатронутого сервиса.- Эффект:
GLITCHTIP_SECRET— разлогинит GlitchTip-сессии;COOKIE_ENCRYPTION_KEY— инвалидирует сохранённый browser-стейт скрапера (потребуется re-login);JWT_SECRET(см. §3, появится после B3-4) — инвалидирует все выданные JWT (все юзеры разлогинятся). Планировать на low-traffic окно. - Vault entry.
Класс G — Proxy creds (SCRAPER_PROXY_URL и др. *_PROXY_URL)
Downtime: нет для основного API; скраперы переключатся на новый прокси при следующем прогоне.
- Получить новый прокси-endpoint/credentials у провайдера (mobileproxy и т.п.).
- Обновить
SCRAPER_PROXY_URL(и/или per-источникCIAN_PROXY_URL/YANDEX_PROXY_URL/AVITO_PROXY_URL) в tradein.env.runtimesed-ом. up -d --force-recreate --no-deps backend scraperв tradein-стеке.- Vault entry.
URL содержит
user:pass@host— это секрет; не логировать целиком.
Класс H — basic_auth (Caddy pilot users)
Downtime: нет. Процедура полностью в docs/runbooks/basic_auth_management.md.
Кратко: scripts/auth/{add,remove}_user.sh → правит caddy/users.caddy.snippet
(bcrypt-хеш, не plaintext) → commit → PR → merge → deploy.yml делает caddy reload.
При компрометации: remove_user.sh ASAP + новый credential + рассылка стейкхолдерам.
3. Особый случай: SCRAPE_ADMIN_TOKEN (issue #78 acceptance)
Issue #78 просит «тестовую ротацию SCRAPE_ADMIN_TOKEN без downtime».
Статус токена: DEPRECATED — app-level admin-auth был удалён в PR #436
(backend/.env.example:30), доступ к админ-эндпоинтам теперь закрыт Caddy basic_auth.
Поле оставлено в core/deps.py только для быстрого rollback.
Вывод: активной ротации не требуется — токен ни на что не влияет, пока
AdminTokenAuth dep не реинстейтнут. Если/когда его вернут, он попадает в класс F
(процедура: sed в backend/.env.runtime → up -d --force-recreate --no-deps backend beat,
downtime отсутствует). Фактический прод-прогон ротации — операционное действие
(Anton), не выполняется в рамках этого PR.
JWT_SECRET (упомянут в #78 «после B3-4») в кодовой базе ещё отсутствует —
добавить в реестр (класс F) при внедрении JWT-аутентификации.
4. Периодичность
| Класс | Плановая ротация | Внеплановая (немедленно) |
|---|---|---|
| A — deploy SSH key | 12 мес | компрометация / уход члена команды / #391 (см. §2-A) |
| B — PAT | 6–12 мес (или по expiry провайдера) | утечка / смена команды |
| C — DSN | по необходимости | утечка DSN |
| D — 3rd-party API | по политике провайдера | утечка / подозрительная активность |
| E — DB password | 12 мес | компрометация / смена команды |
| F — app secret | 12 мес | компрометация |
| G — proxy | по сроку аренды прокси | блокировка/утечка |
| H — basic_auth | конец пилота / per-user | компрометация пароля |
Триггеры внеплановой ротации (любой класс):
- Уход/смена члена команды с доступом к vault или VPS.
- Секрет случайно попал в git / лог / скриншот / чат.
- Подозрение на компрометацию VPS или CI-runner.
5. Audit-чеклист (онбординг / периодический ревью, раз в квартал)
- Все CI-секреты в Forgejo (и GitHub-зеркало для obsidian) актуальны и не истекли.
.env/.env.runtimeна VPS — chmod 600, владелец deploy-юзер (ls -l).- Ни один секрет не закоммичен:
git grep -nE '(password|secret|token|api_key)\s*=\s*\S' -- '*.env*' ':!*.example'пуст в tree. git ls-files | grep -iE '\.pem$|id_rsa|id_ed25519|sshkey'— пусто.- Deploy SSH key из #391 ротирован (старый публичный ключ удалён из
authorized_keys). meta/00_credentials.mdв vault совпадает с фактическими именами из §1 (нет «осиротевших» записей).- Каждая ротация за период имеет audit-entry в vault.
- basic_auth:
scripts/auth/list_users.shсовпадает с актуальным списком пилотов. - Уехавшие члены команды отозваны везде (Forgejo, GitHub, VPS
authorized_keys, vault-доступ).
6. OIDC для CI/CD deploy — вердикт
Запрос #78: перейти на OIDC для Actions-деплоя вместо long-lived DEPLOY_SSH_KEY.
Вердикт: FUTURE (сейчас не реализуемо).
- Деплой во всех трёх пайплайнах (
deploy.yml,deploy-tradein.yml,deploy-obsidian.yml) идёт черезappleboy/ssh-actionпо SSH-ключу на Beget VPS. Это прямой SSH на собственный хост, а не federation в облако (AWS/GCP/Azure) — классический сценарий «GHA OIDC → cloud role» здесь не применим: конечная точка не понимает OIDC-токен, ей нужен SSH. - Forgejo на проде — v10.0.3 (Gitea 1.22 base). В этой версии Forgejo Actions не
предоставляет OIDC-токен-эндпоинт для workflow (
ACTIONS_ID_TOKEN_REQUEST_URL/core.getIDToken()отсутствуют;id-token: writepermission не реализован). Поиск по репо подтверждает: ни одного OIDC/id-tokenупоминания в workflow нет. - Даже на новых Forgejo OIDC-функциональность — это про Forgejo как OIDC-провайдер для внешних сервисов, а не drop-in замена SSH-ключа для деплоя на свой VPS.
Что реально снижает риск SSH-ключа сейчас (рекомендации, не входят в этот PR):
- Ротация ключа из #391 (см. §2-A) — приоритет: ключ скомпрометирован историей.
- Ограничить ключ в
authorized_keysчерезcommand=/restrict/from=<runner-ip>если IP runner-а стабилен (force-command на deploy-скрипт вместо полного shell). - Отдельный deploy-юзер с минимальными правами (не root),
sudoтолько на нужные docker-команды. - Регулярная плановая ротация по классу A (12 мес).
Пересмотреть OIDC, когда: (а) Forgejo обновится до версии с OIDC-token-провайдером
для Actions, и (б) деплой-таргет сможет валидировать OIDC (напр. миграция на облачный
registry/host с IAM). До тех пор — short-lived SSH через command=-ограничение + плановая
ротация закрывают практический риск.
7. Что вне этого документа (Anton territory)
- Реестр значений секретов — vault
meta/00_credentials.md(дополнить именами из §1, если расходится). - Фактическая ротация на проде — операционное действие (SSH на VPS,
ALTER ROLE, правка.env), выполняет владелец инфраструктуры. - Audit-entry per rotation — Obsidian vault.
- Чистка
sshkey.txtиз git-истории (git filter-repo) — отдельная операция; ротация ключа (§2-A) важнее и делает старый blob бесполезным.