docs(infra): secrets rotation policy (#78) #1324
1 changed files with 301 additions and 0 deletions
301
docs/Secrets_Rotation_Policy.md
Normal file
301
docs/Secrets_Rotation_Policy.md
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
# 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 beat` (и `caddy`/`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-клиенты потребуют новый пароль.
|
||||
|
||||
### Класс F — App secrets (`GLITCHTIP_SECRET`, `COOKIE_ENCRYPTION_KEY`, `JWT_SECRET`)
|
||||
|
||||
**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.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: 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 бесполезным.
|
||||
Loading…
Add table
Reference in a new issue