gendesign/docs/Secrets_Rotation_Policy.md
Light1YT d030b04cf2
All checks were successful
CI / changes (push) Successful in 7s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Has been skipped
CI / openapi-codegen-check (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
docs(infra): secrets rotation policy + procedure (#78)
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
2026-06-13 23:48:14 +05:00

24 KiB
Raw Permalink Blame History

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-шага).

  1. На VPS под deploy-юзером сгенерировать новую keypair: ssh-keygen -t ed25519 -C "forgejo-deploy-$(date +%Y%m%d)" -f ~/.ssh/forgejo_deploy_new.
  2. Добавить новый публичный ключ в ~/.ssh/authorized_keys на VPS (рядом со старым — оба валидны на время cutover).
  3. Обновить secret DEPLOY_SSH_KEY (приватная часть) в обоих местах: Forgejo repo secrets и GitHub repo secrets (obsidian-стек использует GitHub Actions).
  4. Прогнать workflow_dispatch на deploy.yml — убедиться, что deploy зелёный с новым ключом.
  5. Удалить старый публичный ключ из authorized_keys.
  6. Vault entry.

⚠️ КРИТИЧНО (исторический инцидент): файл sshkey.txt был закоммичен и удалён в PR #391 (commit 6f37240), но остаётся в git-истории — этот deploy-ключ следует считать скомпрометированным и ротировать по процедуре выше как приоритетную задачу (если ещё не сделано). Удаление файла из tree НЕ убирает blob из истории; чистка истории (git filter-repo) — отдельная операция, но ротация ключа важнее (после ротации старый blob бесполезен).

Класс B — PAT (GHCR_PAT, FORGEJO_BOT_QA_TOKEN, FORGEJO_TOKEN)

Downtime: нет (если новый PAT выпущен до отзыва старого).

  1. Выпустить новый PAT в соответствующем сервисе:
    • GHCR_PAT: GitHub → Settings → Developer settings → Tokens (classic) с write:packages, read:packages.
    • FORGEJO_BOT_QA_TOKEN: войти под bot-qa в Forgejo → Settings → Applications → minimum write:issue.
    • FORGEJO_TOKEN: личный PAT Anton (Forgejo → Applications).
  2. Обновить значение в Forgejo repo secrets (для CI) / в keychain (для CLI).
  3. Триггернуть зависимый workflow (deploy.yml для GHCR, stale-claims.yml для bot-qa) — проверить зелёный.
  4. Отозвать старый PAT в сервисе.
  5. 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, события просто не шлются в окно ротации).

  1. В GlitchTip UI (errors.gendsgn.ru) → Project Settings → Client Keys (DSN) → revoke + create new.
  2. Backend: обновить GLITCHTIP_BACKEND_DSN в Forgejo secrets → deploy пропишет его в .env.runtime через sed и --force-recreate backend/worker/beat.
  3. Frontend: обновить GLITCHTIP_FRONTEND_DSN (build-arg NEXT_PUBLIC_GLITCHTIP_DSN) → требует rebuild frontend образа (запекается на build-time) → workflow_dispatch или push в frontend/**.
  4. Vault entry.

Класс D — 3rd-party API keys (OBJECTIVE_API_KEY, OPENAI_API_KEY, YANDEX_GEOCODER_API_KEY, DADATA_*, CIAN_LOGIN_*)

Downtime: нет (фичи gracefully degrade при пустом ключе — см. config-комментарии).

  1. Перевыпустить/ротировать ключ в кабинете провайдера (Объектив / OpenAI / Yandex Cloud / DaData / Cian-аккаунт).
  2. Где живёт:
    • OBJECTIVE_API_KEY, OPENAI_API_KEY — Forgejo secret → deploy пишет в main .env.runtime.
    • YANDEX_GEOCODER_API_KEY, DADATA_*, CIAN_LOGIN_* — tradein .env.runtime (правится на VPS вручную, не из CI).
  3. Обновить значение sed-ом (НЕ перезапись файла) и up -d --force-recreate --no-deps backend worker beat (main) / ... backend scraper (tradein).
  4. Vault entry.

Класс E — DB passwords (POSTGRES_PASSWORD, TRADEIN_POSTGRES_PASSWORD, *_FDW_PASSWORD, *_READER_PASSWORD, COUCHDB_PASSWORD, GLITCHTIP_DB_PASS)

Downtime: короткий — backend переподключается после смены (несколько секунд).

Главная роль (gendesign / tradein):

  1. Сгенерировать пароль: openssl rand -base64 24 | tr -d '/+' | head -c 24.
  2. ALTER ROLE <role> WITH PASSWORD '<new>'; внутри контейнера postgres (docker compose -p <project> exec postgres psql -U <user> -d <db> -c "ALTER ROLE ...").
  3. Обновить пароль в .env (POSTGRES_PASSWORD) / .env.runtime sed-ом.
  4. up -d --force-recreate --no-deps backend worker beatcaddy/frontend если завязаны).
  5. 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-клиенты потребуют новый пароль.

Downtime / эффект: смена инвалидирует выпущенные артефакты (сессии/токены/cookie).

  1. Сгенерировать: openssl rand -hex 32 (или -base64 48).
  2. Обновить в соответствующем .env/.env.runtime.
  3. up -d --force-recreate затронутого сервиса.
  4. Эффект: GLITCHTIP_SECRET — разлогинит GlitchTip-сессии; COOKIE_ENCRYPTION_KEY — инвалидирует сохранённый browser-стейт скрапера (потребуется re-login); JWT_SECRET (см. §3, появится после B3-4) — инвалидирует все выданные JWT (все юзеры разлогинятся). Планировать на low-traffic окно.
  5. Vault entry.

Класс G — Proxy creds (SCRAPER_PROXY_URL и др. *_PROXY_URL)

Downtime: нет для основного API; скраперы переключатся на новый прокси при следующем прогоне.

  1. Получить новый прокси-endpoint/credentials у провайдера (mobileproxy и т.п.).
  2. Обновить SCRAPER_PROXY_URL (и/или per-источник CIAN_PROXY_URL/YANDEX_PROXY_URL/AVITO_PROXY_URL) в tradein .env.runtime sed-ом.
  3. up -d --force-recreate --no-deps backend scraper в tradein-стеке.
  4. 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.runtimeup -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 612 мес (или по 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: write permission не реализован). Поиск по репо подтверждает: ни одного OIDC/id-token упоминания в workflow нет.
  • Даже на новых Forgejo OIDC-функциональность — это про Forgejo как OIDC-провайдер для внешних сервисов, а не drop-in замена SSH-ключа для деплоя на свой VPS.

Что реально снижает риск SSH-ключа сейчас (рекомендации, не входят в этот PR):

  1. Ротация ключа из #391 (см. §2-A) — приоритет: ключ скомпрометирован историей.
  2. Ограничить ключ в authorized_keys через command=/restrict/from=<runner-ip> если IP runner-а стабилен (force-command на deploy-скрипт вместо полного shell).
  3. Отдельный deploy-юзер с минимальными правами (не root), sudo только на нужные docker-команды.
  4. Регулярная плановая ротация по классу 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 бесполезным.