Skip to content

Runbook — секреты и ротация

Audience: operator. Все секреты хранятся как age-зашифрованный шифротекст в git; расшифровка возможна только приватным ключом, который не в git.

В этом документе — только где лежит секрет и как его крутить. Никаких значений. Никогда не вставлять токен в URL remote и никогда не печатать его в лог/чат.

Модель хранения

  • Репозиторий секретов: приватный kontinuum/secrets (http://gitlab.local.lan/kontinuum/secrets.git). Содержит только файлы *.age (шифротекст age). Клон на воркере: worker:~/secrets-repo.
  • Корневой приватный ключ: worker:~/.git-key (age identity). НЕ в git. Это out-of-band корень доверия: кто его имеет — читает все секреты.
  • Публичный ключ: worker:~/.git-key.pub — им шифруют новые секреты (приватник для добавления не нужен).
plantuml Diagram

Что лежит в хранилище

Клон репозитория секретов на воркере: worker:~/kontinuum-secrets (исторически упоминался ~/secrets-repo — авторитетен фактический клон; репозиторий один: kontinuum/secrets).

Файл (*.age)СодержитПотребитель
git-credentials.ageGitLab PAT (см. ниже)git на воркере
release-key-v2.jks.ageактивный Android release keystore (0.5.1+)подпись APK
release-key-v2.creds.agealias + storepass/keypass для release-key-v2 (сильный, маскируемый) — это и есть текущие CI-vars ANDROID_KEY_ALIAS/ANDROID_KEYSTORE_PASSWORD/ANDROID_KEY_PASSWORDCI-подпись APK
release-key.jks.ageархив — keystore старого ключа (cert d71bd8d6, alias key-alias, слабый 7-симв пароль). Подписал только 0.5.0воспроизводимость/анализ 0.5.0; в CI больше не используется
release-key.creds.agealias + storepass/keypass архивного старого keystore (0.5.0 only)воспроизводимость 0.5.0
updates-deploy-ssh-key.ageCI-var UPDATES_DEPLOY_SSH_KEY (file-type): SSH deploy-ключ публикации APK на updates.kontinuum.cloudCI release-publish
release-publish-token.ageCI-var RELEASE_PUBLISH_TOKENCI release-publish
hygiene-pat.ageCI-var HYGIENE_PATCI hygiene/cleanup job
submodule-sync-token.ageCI-var SUBMODULE_SYNC_TOKENCI рекурсивный submodule-checkout
cf-service-token.ageCF Access service-tokenавтоматический доступ к GitLab API/CI
stalwart.env.ageenv почтового сервера Stalwartmail-сервис

(Список может расти — авторитетен ls worker:~/kontinuum-secrets/*.age.)

Операции (всё на воркере)

Прочитать секрет:

bash
cd ~/secrets-repo
age -d -i ~/.git-key release-key-v2.jks.age > /tmp/release-key.jks   # пример (активный ключ)
# ... использовать, затем удалить расшифрованную копию:
shred -u /tmp/release-key.jks

Добавить / обновить секрет (приватник не нужен — шифруем публичным):

bash
cd ~/secrets-repo
age -r "$(cat ~/.git-key.pub)" -o name.age <plaintext-файл-или-stdin>
git add name.age && git commit -m "secrets: add/rotate name" && git push

GitLab PAT (личный токен)

  • Живёт только в worker:~/.git-credentials (формат http://oauth2:<TOKEN>@gitlab.local.lan). Git использует его через credential helper — remotes остаются без токена в URL.
  • Ротация: выпустить новый PAT в GitLab → отредактировать один файл ~/.git-credentials (заменить токен) → также обновить git-credentials.age в хранилище. Больше нигде токен не дублируется.

Никогда

  • Не встраивать токен в URL remote (git remote set-url …token…).
  • Не печатать токен (echo, cat ~/.git-credentials, ps с cloudflared).
  • Для извлечения PAT в скрипте — sed-выемка в переменную, не в stdout.

CI-секреты

Секреты, нужные пайплайну, продублированы как GitLab CI-переменные (masked) в настройках проекта/группы. То есть один логический секрет может жить в двух местах: age-хранилище (источник истины для людей/воркера) и masked CI-var (для раннера). При ротации обновлять оба.

Корневой ключ — критично

worker:~/.git-keyединственная копия корня. Если воркер умрёт и off-machine копии нет — все in-git секреты потеряны (шифротекст нерасшифруем).

Обязательство

~/.git-key обязан быть забэкаплен off-machine (менеджер паролей). Это не «желательно» — это условие выживания всего секрет-стора. Проверка: операционно подтверждать наличие копии в менеджере паролей. См. также runbook бэкапов.

CI ↔ age карта (проект 23, аудит #75 + миграция v2 #118 C — 2026-06-24)

Каждый CI-секрет проекта 23 теперь имеет age-бэкап в kontinuum/secrets. Один логический секрет живёт в двух местах: age-хранилище (источник истины для людей/воркера) и CI-var (для раннера). При ротации обновлять оба.

CI-varmaskedprotectedtypeage-файл
ANDROID_KEYSTORE_B64нет (слишком длинный — лимит маскирования GitLab)даenvrelease-key-v2.jks.age (cert 954bcd2e)
ANDROID_KEYSTORE_PASSWORDда (v2: 32 симв ≥ 8 → маскируется)даenvrelease-key-v2.creds.age
ANDROID_KEY_PASSWORDда (v2: 32 симв ≥ 8 → маскируется)даenvrelease-key-v2.creds.age
ANDROID_KEY_ALIASнет (не секрет; = kontinuum-release-v2)даenvrelease-key-v2.creds.age
SUBMODULE_SYNC_TOKENдадаenvsubmodule-sync-token.age
HYGIENE_PATдадаenvhygiene-pat.age
UPDATES_DEPLOY_SSH_KEYнет (file-vars маскировать нельзя; не эхоятся)даfileupdates-deploy-ssh-key.age
RELEASE_PUBLISH_TOKENдадаenvrelease-publish-token.age

✅ Закрыто: keystore-пароли маскируются (#75 через #118 C)

Ранее ANDROID_KEYSTORE_PASSWORD/ANDROID_KEY_PASSWORD были по 7 символов (< 8 → GitLab отвергал masked=true). После миграции CI на release-key-v2 (2026-06-24, #118 решение C) оба пароля — по 32 символа → GitLab принял masked=true. Гэп #75 закрыт: оба пароля теперь маскируются в логах.

✅ Разрешено: CI подписывает ключом v2 (#118 решение C)

CI (проект 23) теперь подписывает release-key-v2 (cert 954bcd2e…, alias kontinuum-release-v2, сильный 32-симв пароль). ANDROID_KEYSTORE_B64/ANDROID_*_PASSWORD/ANDROID_KEY_ALIAS обновлены на v2 через API (2026-06-24, sudo=cto) и проверены: реальный release-APK, переподписанный текущими CI-vars, верифицируется как Signer #1 certificate SHA-256: 954bcd2e…, не старый d71bd8d6. Старый release-key.jks — архив (подписал только 0.5.0).

⚠ Continuity намеренно сломана на 0.5.0→0.5.1 (#118 C)

v2 — другой signing-cert, чем 0.5.0 (d71bd8d6). Android обновляет APK только при совпадении signing-cert, поэтому 0.5.1 НЕ установится поверх 0.5.0 как апдейт. Это сознательное решение founder (#118, вариант C): база юзеров ≈ 0 (только founder, pre-launch), простота ухода на сильный ключ важнее continuity.

Founder ОБЯЗАН переустановить: удалить 0.5.0 и поставить 0.5.1 с нуля (авто-апдейт с 0.5.0 на 0.5.1 не сработает). Делать это один раз, когда 0.5.1 будет реально собран и опубликован. Последующие релизы (0.5.1→0.5.2…) снова идут авто-апдейтом — все на v2.

Известные пробелы (не покрыто age-бэкапом)

  • bill .env — на воркере не найден; стек bill спящий/выключен. Забэкапить при провижене bill.

Порядок восстановления (worker погиб)

Предпосылка: out-of-band корень (~/.git-key + GitLab-логин) восстановлен из менеджера паролей founder.

  1. GitLab-доступ. Войти в GitLab (логин из менеджера паролей). Выпустить/ восстановить PAT → положить в worker:~/.git-credentials (http://oauth2:<TOKEN>@gitlab.local.lan). Без токена в URL remote.
  2. Корневой age-ключ. Восстановить worker:~/.git-key (0600) и ~/.git-key.pub из менеджера паролей. Проверка: age -d -i ~/.git-key ~/kontinuum-secrets/cf-service-token.age >/dev/null.
  3. Клон хранилища. git clone … kontinuum/secrets.git ~/kontinuum-secrets.
  4. Подпись релизов. Расшифровать release-key-v2.jks.age + release-key-v2.creds.age → восстановить CI-vars ANDROID_KEYSTORE_B64 (base64 jks, protected, masked=false), ANDROID_KEYSTORE_PASSWORD + ANDROID_KEY_PASSWORD (masked=true), ANDROID_KEY_ALIAS (kontinuum-release-v2) через API проекта 23. Проверка: cert восстановленного keystore = 954bcd2e…. (Старый release-key.*.age — только для воспроизведения архивного 0.5.0, в CI не нужен.)
  5. CI-токены. Восстановить SUBMODULE_SYNC_TOKEN, HYGIENE_PAT, RELEASE_PUBLISH_TOKEN (env, masked) и UPDATES_DEPLOY_SSH_KEY (variable_type=file) из соответствующих .age через API.
  6. Инфра-сервисы. cf-service-token.age → CF Access; stalwart.env.age/etc/stalwart/ на head.
  7. Проверка. Прогнать release-пайплайн (сборка+подпись+публикация APK).

Founder TODO (out-of-band корень)

Подтвердить, что в менеджере паролей лежат обе копии корня: (1) приватный age-ключ ~/.git-key (целиком), (2) GitLab-логин/восстановление. Без них шифротекст в git нерасшифруем — это условие выживания всего стора.